@quazardous/qdadm 1.19.6 → 1.20.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 +12 -2
- package/package.json +2 -1
- package/src/components/index.ts +1 -0
- package/src/components/show/ParentCard.vue +112 -0
- package/src/composables/createShowFieldResolver.ts +170 -0
- package/src/composables/useEntityItemShowPage.ts +15 -123
- package/src/i18n/defaults/DefaultCoreProvider.ts +12 -8
- package/src/i18n/defaults/__tests__/generated-sync.test.ts +42 -0
- package/src/i18n/defaults/core.en.generated.ts +58 -0
- package/src/i18n/defaults/core.fr.generated.ts +58 -0
- package/src/i18n/loaders/yaml.ts +6 -3
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 { useForm, useBareForm,
|
|
34
|
+
import { useForm, useBareForm, useListPage } from '@quazardous/qdadm/composables'
|
|
35
35
|
|
|
36
36
|
// Components
|
|
37
37
|
import { ListPage, PageLayout, AppLayout, FormField, FormActions } from '@quazardous/qdadm/components'
|
|
@@ -96,8 +96,18 @@ Options: `transformRequest`, `transformResponse`, `responseFormat`, `clientSideP
|
|
|
96
96
|
| Doc | Purpose |
|
|
97
97
|
|-----|---------|
|
|
98
98
|
| [QDADM_CREDO](QDADM_CREDO.md) | Philosophy, patterns, concepts |
|
|
99
|
+
| [Page compositions](../../docs/page-compositions.md) | **Start here** — which composition for which need |
|
|
100
|
+
| [CRUD Pages](../../docs/crud.md) | List / form / show / child pages reference |
|
|
99
101
|
| [Architecture](../../docs/architecture.md) | PAC pattern, layers |
|
|
100
|
-
| [Extension](../../docs/extension.md) |
|
|
102
|
+
| [Extension](../../docs/extension.md) | Decorators, bundles, multi-storage |
|
|
103
|
+
| [Hooks](../../docs/hooks.md) | Alter / invoke registry |
|
|
104
|
+
| [Signals](../../docs/signals.md) | Event bus, SSE bridge |
|
|
105
|
+
| [Zones](../../docs/zones.md) | Extensible UI blocks |
|
|
106
|
+
| [Security](../../docs/security.md) | Permissions, roles |
|
|
107
|
+
| [i18n](../../docs/i18n.md) | Messages, providers, strategies |
|
|
108
|
+
| [Codegen](../../docs/gen.md) | Generate managers (OpenAPI / manual) |
|
|
109
|
+
| [Deferred](../../docs/deferred.md) | Deferred values & warmup |
|
|
110
|
+
| [AGENT_GUIDE](../../docs/AGENT_GUIDE.md) | Quick index for AI agents |
|
|
101
111
|
|
|
102
112
|
## Peer Dependencies
|
|
103
113
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quazardous/qdadm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "Vue 3 framework for admin dashboards with PrimeVue",
|
|
5
5
|
"author": "quazardous",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"test": "vitest run",
|
|
19
19
|
"test:watch": "vitest",
|
|
20
|
+
"gen:i18n-defaults": "node scripts/gen-i18n-defaults.mjs",
|
|
20
21
|
"type-check": "vue-tsc --noEmit",
|
|
21
22
|
"lint": "eslint src/",
|
|
22
23
|
"lint:fix": "eslint src/ --fix",
|
package/src/components/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export { default as LookupPickerDialog } from './edit/LookupPickerDialog.vue'
|
|
|
36
36
|
export { default as ShowPage } from './show/ShowPage.vue'
|
|
37
37
|
export { default as ShowField } from './show/ShowField.vue'
|
|
38
38
|
export { default as ShowDisplay } from './show/ShowDisplay.vue'
|
|
39
|
+
export { default as ParentCard } from './show/ParentCard.vue'
|
|
39
40
|
|
|
40
41
|
// Lists
|
|
41
42
|
export { default as ListPage } from './lists/ListPage.vue'
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ParentCard - normalized parent detail embedded at the top of a child ListPage
|
|
4
|
+
*
|
|
5
|
+
* The "B2" hybrid composition (see docs/page-compositions.md): a child list is
|
|
6
|
+
* the host page, and the parent record is shown as a read-only cartouche above
|
|
7
|
+
* the table. `useListPage` already resolves and exposes the parent on a child
|
|
8
|
+
* route (`parentData` / `parentLoading`) — this component just renders it, with
|
|
9
|
+
* the SAME field resolver as `ShowPage` so the look matches real detail pages.
|
|
10
|
+
* No extra fetch.
|
|
11
|
+
*
|
|
12
|
+
* Fields are auto-derived from the parent entity's manager. Pass `fields` to
|
|
13
|
+
* restrict/order them, or use the default slot to render the parent yourself.
|
|
14
|
+
*
|
|
15
|
+
* Usage (inside a child ListPage):
|
|
16
|
+
* <template #beforeTable>
|
|
17
|
+
* <ParentCard
|
|
18
|
+
* :entity="'books'"
|
|
19
|
+
* :data="list.parentData.value"
|
|
20
|
+
* :loading="list.parentLoading.value"
|
|
21
|
+
* :fields="['title', 'author']"
|
|
22
|
+
* />
|
|
23
|
+
* </template>
|
|
24
|
+
*/
|
|
25
|
+
import { computed, type PropType } from 'vue'
|
|
26
|
+
import { useOrchestrator } from '../../orchestrator/useOrchestrator'
|
|
27
|
+
import { useFieldManager } from '../../composables/useFieldManager'
|
|
28
|
+
import {
|
|
29
|
+
createShowFieldResolver,
|
|
30
|
+
type ShowResolvedFieldConfig,
|
|
31
|
+
} from '../../composables/createShowFieldResolver'
|
|
32
|
+
import ShowField from './ShowField.vue'
|
|
33
|
+
|
|
34
|
+
const props = defineProps({
|
|
35
|
+
/** Parent entity name (route-stable). */
|
|
36
|
+
entity: { type: String, required: true },
|
|
37
|
+
/** Parent record (e.g. `list.parentData.value`). */
|
|
38
|
+
data: { type: Object as PropType<Record<string, unknown> | null>, default: null },
|
|
39
|
+
/** Parent loading state (e.g. `list.parentLoading.value`). */
|
|
40
|
+
loading: { type: Boolean, default: false },
|
|
41
|
+
/** Restrict/order the displayed fields. Omit to show all manager fields. */
|
|
42
|
+
fields: { type: Array as PropType<string[] | null>, default: null },
|
|
43
|
+
/** Exclude these fields (applied when `fields` is not set). */
|
|
44
|
+
exclude: { type: Array as PropType<string[]>, default: () => [] },
|
|
45
|
+
/** Render fields horizontally (label beside value). Default true. */
|
|
46
|
+
horizontal: { type: Boolean, default: true },
|
|
47
|
+
/** Optional heading above the fields. */
|
|
48
|
+
title: { type: String, default: '' },
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const { orchestrator, getManager } = useOrchestrator()
|
|
52
|
+
const manager = getManager(props.entity)
|
|
53
|
+
|
|
54
|
+
// Same resolver as ShowPage → identical rendering (types, references, severity).
|
|
55
|
+
const fieldManager = useFieldManager<ShowResolvedFieldConfig>({
|
|
56
|
+
resolveFieldConfig: createShowFieldResolver(manager, orchestrator),
|
|
57
|
+
getSchemaFieldConfig: (name) => manager.getFieldConfig?.(name) || null,
|
|
58
|
+
entity: props.entity,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
fieldManager.generateFields(manager.getFormFields?.() || [], {
|
|
62
|
+
only: props.fields ?? null,
|
|
63
|
+
exclude: props.fields ? [] : props.exclude,
|
|
64
|
+
})
|
|
65
|
+
// Honor the caller's field order when an explicit list is given.
|
|
66
|
+
if (props.fields) fieldManager.setFieldOrder(props.fields)
|
|
67
|
+
|
|
68
|
+
const resolvedFields = fieldManager.fields
|
|
69
|
+
const hasData = computed(() => props.data !== null && props.data !== undefined)
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<div class="parent-card">
|
|
74
|
+
<div v-if="loading" class="parent-card__loading">
|
|
75
|
+
<i class="pi pi-spin pi-spinner" />
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<template v-else-if="hasData">
|
|
79
|
+
<div v-if="title" class="parent-card__title">{{ title }}</div>
|
|
80
|
+
<!-- Escape hatch: render the parent yourself with the resolved fields. -->
|
|
81
|
+
<slot :fields="resolvedFields" :data="data">
|
|
82
|
+
<div class="parent-card__fields">
|
|
83
|
+
<ShowField
|
|
84
|
+
v-for="f in resolvedFields"
|
|
85
|
+
:key="f.name"
|
|
86
|
+
:field="f"
|
|
87
|
+
:value="(data?.[f.name] ?? null) as any"
|
|
88
|
+
:horizontal="horizontal"
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
</slot>
|
|
92
|
+
</template>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<style scoped>
|
|
97
|
+
.parent-card {
|
|
98
|
+
margin-bottom: 1rem;
|
|
99
|
+
}
|
|
100
|
+
.parent-card__loading {
|
|
101
|
+
padding: 0.5rem 0;
|
|
102
|
+
}
|
|
103
|
+
.parent-card__title {
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
margin-bottom: 0.5rem;
|
|
106
|
+
}
|
|
107
|
+
.parent-card__fields {
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
gap: 0.25rem;
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createShowFieldResolver - shared read-only field resolver
|
|
3
|
+
*
|
|
4
|
+
* Factory producing the field resolver used by read-only display surfaces
|
|
5
|
+
* (`useEntityItemShowPage`, `<ParentCard>`). Given an entity manager and the
|
|
6
|
+
* orchestrator, it maps a raw schema field config to a resolved display config:
|
|
7
|
+
*
|
|
8
|
+
* - schema type → display type (via {@link SHOW_TYPE_MAPPINGS})
|
|
9
|
+
* - auto reference route when a field points at another entity
|
|
10
|
+
* - auto severity (badge) from the manager's severity maps
|
|
11
|
+
*
|
|
12
|
+
* Extracted so the parent cartouche of an embedded child list (#1038) renders
|
|
13
|
+
* fields exactly like a real `ShowPage` — same resolver, same look — without
|
|
14
|
+
* duplicating the mapping logic.
|
|
15
|
+
*/
|
|
16
|
+
import { snakeCaseToTitle } from './useFieldManager'
|
|
17
|
+
|
|
18
|
+
/** Severity descriptor returned by a manager's severity maps. */
|
|
19
|
+
type SeverityDescriptor = string | { severity: string; icon?: string; label?: string }
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Minimal manager surface the resolver needs — kept structural (not the full
|
|
23
|
+
* generic `EntityManager`) so it accepts any concrete manager without variance
|
|
24
|
+
* friction.
|
|
25
|
+
*/
|
|
26
|
+
export interface ShowResolverManager {
|
|
27
|
+
routePrefix?: string
|
|
28
|
+
idField: string
|
|
29
|
+
hasSeverityMap?: (name: string) => boolean
|
|
30
|
+
getSeverityDescriptor?: (name: string, value: string | number, fallback: string) => SeverityDescriptor
|
|
31
|
+
getSeverity?: (name: string, value: string | number, fallback: string) => SeverityDescriptor
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Field definition from an EntityManager schema.
|
|
36
|
+
*/
|
|
37
|
+
export interface ShowFieldDefinition {
|
|
38
|
+
name: string
|
|
39
|
+
type?: string
|
|
40
|
+
schemaType?: string
|
|
41
|
+
label?: string
|
|
42
|
+
reference?: string
|
|
43
|
+
referenceRoute?: string | ((value: unknown) => { name: string; params: Record<string, unknown> })
|
|
44
|
+
referenceLabel?: string | ((value: unknown, option?: unknown) => string)
|
|
45
|
+
options?: unknown[]
|
|
46
|
+
optionLabel?: string
|
|
47
|
+
optionValue?: string
|
|
48
|
+
// Display options
|
|
49
|
+
dateFormat?: string
|
|
50
|
+
currencyCode?: string
|
|
51
|
+
locale?: string
|
|
52
|
+
booleanLabels?: { true: string; false: string }
|
|
53
|
+
severity?: string | ((value: unknown) => string | { severity: string; icon?: string; label?: string })
|
|
54
|
+
imageWidth?: string
|
|
55
|
+
imageHeight?: string
|
|
56
|
+
render?: (value: unknown) => string
|
|
57
|
+
[key: string]: unknown
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolved field configuration for display rendering.
|
|
62
|
+
*/
|
|
63
|
+
export interface ShowResolvedFieldConfig extends ShowFieldDefinition {
|
|
64
|
+
name: string
|
|
65
|
+
type: string
|
|
66
|
+
schemaType: string
|
|
67
|
+
label: string
|
|
68
|
+
group?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Minimal orchestrator surface the resolver needs (reference-route lookup).
|
|
73
|
+
*/
|
|
74
|
+
export interface ShowResolverOrchestrator {
|
|
75
|
+
get: (entityName: string) => ShowResolverManager | undefined
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Schema type → display type mapping.
|
|
80
|
+
*/
|
|
81
|
+
export const SHOW_TYPE_MAPPINGS: Record<string, string> = {
|
|
82
|
+
text: 'text',
|
|
83
|
+
string: 'text',
|
|
84
|
+
email: 'email',
|
|
85
|
+
password: 'password',
|
|
86
|
+
number: 'number',
|
|
87
|
+
integer: 'number',
|
|
88
|
+
float: 'number',
|
|
89
|
+
decimal: 'number',
|
|
90
|
+
currency: 'currency',
|
|
91
|
+
boolean: 'boolean',
|
|
92
|
+
bool: 'boolean',
|
|
93
|
+
date: 'date',
|
|
94
|
+
datetime: 'datetime',
|
|
95
|
+
time: 'datetime',
|
|
96
|
+
timestamp: 'datetime',
|
|
97
|
+
select: 'select',
|
|
98
|
+
enum: 'select',
|
|
99
|
+
textarea: 'textarea',
|
|
100
|
+
longtext: 'textarea',
|
|
101
|
+
reference: 'reference',
|
|
102
|
+
foreignKey: 'reference',
|
|
103
|
+
image: 'image',
|
|
104
|
+
url: 'url',
|
|
105
|
+
link: 'url',
|
|
106
|
+
json: 'json',
|
|
107
|
+
object: 'json',
|
|
108
|
+
array: 'json',
|
|
109
|
+
badge: 'badge',
|
|
110
|
+
tag: 'badge',
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build a show-style field resolver bound to a manager + orchestrator.
|
|
115
|
+
*/
|
|
116
|
+
export function createShowFieldResolver(
|
|
117
|
+
manager: ShowResolverManager,
|
|
118
|
+
orchestrator: ShowResolverOrchestrator
|
|
119
|
+
): (name: string, fieldConfig: Partial<ShowFieldDefinition>) => ShowResolvedFieldConfig {
|
|
120
|
+
return function resolveFieldConfig(
|
|
121
|
+
name: string,
|
|
122
|
+
fieldConfig: Partial<ShowFieldDefinition>
|
|
123
|
+
): ShowResolvedFieldConfig {
|
|
124
|
+
const {
|
|
125
|
+
type = 'text',
|
|
126
|
+
schemaType = type,
|
|
127
|
+
label = '',
|
|
128
|
+
reference = '',
|
|
129
|
+
severity,
|
|
130
|
+
...rest
|
|
131
|
+
} = fieldConfig
|
|
132
|
+
|
|
133
|
+
let resolvedDisplayType = SHOW_TYPE_MAPPINGS[type] || SHOW_TYPE_MAPPINGS[schemaType] || 'text'
|
|
134
|
+
const resolvedLabel = label || snakeCaseToTitle(name)
|
|
135
|
+
|
|
136
|
+
// Auto-set reference route if reference entity is specified
|
|
137
|
+
let referenceRoute = rest.referenceRoute
|
|
138
|
+
if (reference && !referenceRoute) {
|
|
139
|
+
const refManager = orchestrator.get(reference)
|
|
140
|
+
if (refManager) {
|
|
141
|
+
referenceRoute = (value: unknown) => ({
|
|
142
|
+
name: `${refManager.routePrefix || reference}-show`,
|
|
143
|
+
params: { [refManager.idField]: value },
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Auto-inject severity from manager's severity maps
|
|
149
|
+
let resolvedSeverity = severity
|
|
150
|
+
if (!resolvedSeverity && manager.hasSeverityMap?.(name)) {
|
|
151
|
+
resolvedSeverity = manager.getSeverityDescriptor
|
|
152
|
+
? (value: unknown) => manager.getSeverityDescriptor!(name, value as string | number, 'secondary')
|
|
153
|
+
: (value: unknown) => manager.getSeverity!(name, value as string | number, 'secondary')
|
|
154
|
+
if (resolvedDisplayType === 'text') {
|
|
155
|
+
resolvedDisplayType = 'badge'
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
name,
|
|
161
|
+
type: resolvedDisplayType,
|
|
162
|
+
schemaType,
|
|
163
|
+
label: resolvedLabel,
|
|
164
|
+
reference,
|
|
165
|
+
referenceRoute,
|
|
166
|
+
severity: resolvedSeverity,
|
|
167
|
+
...rest,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -33,24 +33,27 @@
|
|
|
33
33
|
* <ShowPage v-bind="show.props.value" v-on="show.events" />
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
|
-
import { ref, computed,
|
|
37
|
-
import { useRouter,
|
|
36
|
+
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
|
37
|
+
import { useRouter, type Router } from 'vue-router'
|
|
38
38
|
import { useConfirm } from 'primevue/useconfirm'
|
|
39
39
|
import {
|
|
40
40
|
useEntityItemPage,
|
|
41
41
|
type ParentConfig,
|
|
42
42
|
type EntityManager,
|
|
43
|
-
type UseEntityItemPageReturn,
|
|
44
43
|
} from './useEntityItemPage'
|
|
45
44
|
import type { StackHydratorReturn } from '../chain/useStackHydrator'
|
|
46
45
|
import {
|
|
47
46
|
useFieldManager,
|
|
48
|
-
snakeCaseToTitle,
|
|
49
47
|
type FieldGroup,
|
|
50
48
|
type GroupDefinition,
|
|
51
49
|
type GroupOptions,
|
|
52
50
|
type AddFieldOptions,
|
|
53
51
|
} from './useFieldManager'
|
|
52
|
+
import {
|
|
53
|
+
createShowFieldResolver,
|
|
54
|
+
type ShowFieldDefinition,
|
|
55
|
+
type ShowResolvedFieldConfig,
|
|
56
|
+
} from './createShowFieldResolver'
|
|
54
57
|
|
|
55
58
|
/**
|
|
56
59
|
* Orchestrator interface
|
|
@@ -66,41 +69,16 @@ interface Orchestrator {
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
/**
|
|
69
|
-
* Field definition from EntityManager schema
|
|
72
|
+
* Field definition from EntityManager schema.
|
|
73
|
+
* Re-exported from the shared resolver under the historical name.
|
|
70
74
|
*/
|
|
71
|
-
export
|
|
72
|
-
name: string
|
|
73
|
-
type?: string
|
|
74
|
-
schemaType?: string
|
|
75
|
-
label?: string
|
|
76
|
-
reference?: string
|
|
77
|
-
referenceRoute?: string | ((value: unknown) => { name: string; params: Record<string, unknown> })
|
|
78
|
-
referenceLabel?: string | ((value: unknown, option?: unknown) => string)
|
|
79
|
-
options?: unknown[]
|
|
80
|
-
optionLabel?: string
|
|
81
|
-
optionValue?: string
|
|
82
|
-
// Display options
|
|
83
|
-
dateFormat?: string
|
|
84
|
-
currencyCode?: string
|
|
85
|
-
locale?: string
|
|
86
|
-
booleanLabels?: { true: string; false: string }
|
|
87
|
-
severity?: string | ((value: unknown) => string | { severity: string; icon?: string; label?: string })
|
|
88
|
-
imageWidth?: string
|
|
89
|
-
imageHeight?: string
|
|
90
|
-
render?: (value: unknown) => string
|
|
91
|
-
[key: string]: unknown
|
|
92
|
-
}
|
|
75
|
+
export type FieldDefinition = ShowFieldDefinition
|
|
93
76
|
|
|
94
77
|
/**
|
|
95
|
-
* Resolved field configuration for display rendering
|
|
78
|
+
* Resolved field configuration for display rendering.
|
|
79
|
+
* Re-exported from the shared resolver under the historical name.
|
|
96
80
|
*/
|
|
97
|
-
export
|
|
98
|
-
name: string
|
|
99
|
-
type: string
|
|
100
|
-
schemaType: string
|
|
101
|
-
label: string
|
|
102
|
-
group?: string
|
|
103
|
-
}
|
|
81
|
+
export type ResolvedFieldConfig = ShowResolvedFieldConfig
|
|
104
82
|
|
|
105
83
|
// FieldGroup, GroupDefinition, GroupOptions imported from useFieldManager
|
|
106
84
|
|
|
@@ -176,43 +154,6 @@ export interface ShowPageEvents {
|
|
|
176
154
|
back: () => void
|
|
177
155
|
}
|
|
178
156
|
|
|
179
|
-
/**
|
|
180
|
-
* Type mapping from schema types to display types
|
|
181
|
-
*/
|
|
182
|
-
const TYPE_MAPPINGS: Record<string, string> = {
|
|
183
|
-
text: 'text',
|
|
184
|
-
string: 'text',
|
|
185
|
-
email: 'email',
|
|
186
|
-
password: 'password',
|
|
187
|
-
number: 'number',
|
|
188
|
-
integer: 'number',
|
|
189
|
-
float: 'number',
|
|
190
|
-
decimal: 'number',
|
|
191
|
-
currency: 'currency',
|
|
192
|
-
boolean: 'boolean',
|
|
193
|
-
bool: 'boolean',
|
|
194
|
-
date: 'date',
|
|
195
|
-
datetime: 'datetime',
|
|
196
|
-
time: 'datetime',
|
|
197
|
-
timestamp: 'datetime',
|
|
198
|
-
select: 'select',
|
|
199
|
-
enum: 'select',
|
|
200
|
-
textarea: 'textarea',
|
|
201
|
-
longtext: 'textarea',
|
|
202
|
-
reference: 'reference',
|
|
203
|
-
foreignKey: 'reference',
|
|
204
|
-
image: 'image',
|
|
205
|
-
url: 'url',
|
|
206
|
-
link: 'url',
|
|
207
|
-
json: 'json',
|
|
208
|
-
object: 'json',
|
|
209
|
-
array: 'json',
|
|
210
|
-
badge: 'badge',
|
|
211
|
-
tag: 'badge',
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// snakeCaseToTitle imported from useFieldManager
|
|
215
|
-
|
|
216
157
|
/**
|
|
217
158
|
* Options for useEntityItemShowPage
|
|
218
159
|
*/
|
|
@@ -327,7 +268,6 @@ export function useEntityItemShowPage<T = unknown>(
|
|
|
327
268
|
const { entity, loadOnMount = true, transformLoad, onLoadSuccess, onLoadError } = options
|
|
328
269
|
|
|
329
270
|
const router: Router = useRouter()
|
|
330
|
-
const route = useRoute()
|
|
331
271
|
const confirm = useConfirm() as ConfirmService
|
|
332
272
|
|
|
333
273
|
// Lazy action state (declared before base so onLoadSuccess wrapper can reference it)
|
|
@@ -376,56 +316,8 @@ export function useEntityItemShowPage<T = unknown>(
|
|
|
376
316
|
|
|
377
317
|
// ============ FIELD & GROUP MANAGEMENT (via useFieldManager) ============
|
|
378
318
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
*/
|
|
382
|
-
function resolveFieldConfig(name: string, fieldConfig: Partial<FieldDefinition>): ResolvedFieldConfig {
|
|
383
|
-
const {
|
|
384
|
-
type = 'text',
|
|
385
|
-
schemaType = type,
|
|
386
|
-
label = '',
|
|
387
|
-
reference = '',
|
|
388
|
-
severity,
|
|
389
|
-
...rest
|
|
390
|
-
} = fieldConfig
|
|
391
|
-
|
|
392
|
-
let resolvedDisplayType = TYPE_MAPPINGS[type] || TYPE_MAPPINGS[schemaType] || 'text'
|
|
393
|
-
const resolvedLabel = label || snakeCaseToTitle(name)
|
|
394
|
-
|
|
395
|
-
// Auto-set reference route if reference entity is specified
|
|
396
|
-
let referenceRoute = rest.referenceRoute
|
|
397
|
-
if (reference && !referenceRoute) {
|
|
398
|
-
const refManager = orchestrator.get(reference)
|
|
399
|
-
if (refManager) {
|
|
400
|
-
referenceRoute = (value: unknown) => ({
|
|
401
|
-
name: `${refManager.routePrefix || reference}-show`,
|
|
402
|
-
params: { [refManager.idField]: value },
|
|
403
|
-
})
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Auto-inject severity from manager's severity maps
|
|
408
|
-
let resolvedSeverity = severity
|
|
409
|
-
if (!resolvedSeverity && manager.hasSeverityMap?.(name)) {
|
|
410
|
-
resolvedSeverity = manager.getSeverityDescriptor
|
|
411
|
-
? (value: unknown) => manager.getSeverityDescriptor!(name, value as string | number, 'secondary')
|
|
412
|
-
: (value: unknown) => manager.getSeverity!(name, value as string | number, 'secondary')
|
|
413
|
-
if (resolvedDisplayType === 'text') {
|
|
414
|
-
resolvedDisplayType = 'badge'
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return {
|
|
419
|
-
name,
|
|
420
|
-
type: resolvedDisplayType,
|
|
421
|
-
schemaType,
|
|
422
|
-
label: resolvedLabel,
|
|
423
|
-
reference,
|
|
424
|
-
referenceRoute,
|
|
425
|
-
severity: resolvedSeverity,
|
|
426
|
-
...rest,
|
|
427
|
-
}
|
|
428
|
-
}
|
|
319
|
+
// Show-specific field resolver (shared with <ParentCard>, see #1038)
|
|
320
|
+
const resolveFieldConfig = createShowFieldResolver(manager, orchestrator)
|
|
429
321
|
|
|
430
322
|
// Create field manager with show-specific resolver
|
|
431
323
|
const fieldManager = useFieldManager<ResolvedFieldConfig>({
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
* Default core provider — ships qdadm's `core.*` defaults (EN + FR) as a
|
|
3
3
|
* `LazyTranslationProvider` configured with a YAML loader.
|
|
4
4
|
*
|
|
5
|
-
* The YAML
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* The YAML text is imported from `core.<locale>.generated.ts` — plain TS
|
|
6
|
+
* modules that `export default` the raw YAML string, generated from the
|
|
7
|
+
* sibling `core.<locale>.yml` source-of-truth (see `scripts/gen-i18n-defaults.mjs`).
|
|
8
|
+
* Using a normal module instead of a Vite `?raw` query keeps the import
|
|
9
|
+
* portable across bundlers and avoids the file:/symlink `server.fs.allow`
|
|
10
|
+
* trap (qdadm #492). The dynamic-import boundary still gives per-locale
|
|
11
|
+
* code-splitting, and the `.yml` files stay translator-friendly.
|
|
9
12
|
*
|
|
10
|
-
* Adding a built-in locale = drop `core.<locale>.yml` next to this file
|
|
11
|
-
*
|
|
13
|
+
* Adding a built-in locale = drop `core.<locale>.yml` next to this file,
|
|
14
|
+
* run `npm run gen:i18n-defaults -w packages/qdadm`, and add the locale to
|
|
15
|
+
* the loader map below.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import { LazyTranslationProvider } from '../LazyTranslationProvider'
|
|
@@ -16,8 +20,8 @@ import { createYamlLoader } from '../loaders/yaml'
|
|
|
16
20
|
|
|
17
21
|
export function createDefaultCoreProvider(): LazyTranslationProvider {
|
|
18
22
|
const yamlLoader = createYamlLoader({
|
|
19
|
-
en: () => import('./core.en.
|
|
20
|
-
fr: () => import('./core.fr.
|
|
23
|
+
en: () => import('./core.en.generated'),
|
|
24
|
+
fr: () => import('./core.fr.generated'),
|
|
21
25
|
})
|
|
22
26
|
|
|
23
27
|
return new LazyTranslationProvider({
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards the `core.<locale>.yml` → `core.<locale>.generated.ts` pipeline
|
|
3
|
+
* introduced for qdadm #492 (drop the Vite-specific `?raw` import).
|
|
4
|
+
*
|
|
5
|
+
* - Drift guard: the generated module's default export must equal the raw
|
|
6
|
+
* `.yml` text verbatim. The generator wraps the raw text in a template
|
|
7
|
+
* literal, which round-trips back to the original — so if you edit a `.yml`
|
|
8
|
+
* without running `npm run gen:i18n-defaults`, this fails.
|
|
9
|
+
* - End-to-end: the default core provider must load its bundles through the
|
|
10
|
+
* new (non-`?raw`) import path.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
15
|
+
import { describe, it, expect } from 'vitest'
|
|
16
|
+
|
|
17
|
+
import enGenerated from '../core.en.generated'
|
|
18
|
+
import frGenerated from '../core.fr.generated'
|
|
19
|
+
import { createDefaultCoreProvider } from '../DefaultCoreProvider'
|
|
20
|
+
|
|
21
|
+
const defaultsDir = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
22
|
+
const readYml = (locale: string) =>
|
|
23
|
+
readFileSync(join(defaultsDir, `core.${locale}.yml`), 'utf8')
|
|
24
|
+
|
|
25
|
+
describe('i18n default providers — generated/.yml sync', () => {
|
|
26
|
+
it('core.en.generated.ts round-trips core.en.yml', () => {
|
|
27
|
+
expect(enGenerated).toBe(readYml('en'))
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('core.fr.generated.ts round-trips core.fr.yml', () => {
|
|
31
|
+
expect(frGenerated).toBe(readYml('fr'))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('default core provider loads bundles via the generated modules', async () => {
|
|
35
|
+
type CoreBundle = { core: { actions: { save: string } } }
|
|
36
|
+
const provider = createDefaultCoreProvider()
|
|
37
|
+
const en = (await provider.load('en')) as CoreBundle
|
|
38
|
+
const fr = (await provider.load('fr')) as CoreBundle
|
|
39
|
+
expect(en.core.actions.save).toBe('Save')
|
|
40
|
+
expect(fr.core.actions.save).toBe('Enregistrer')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* GENERATED from core.en.yml — DO NOT EDIT.
|
|
2
|
+
* Run `npm run gen:i18n-defaults -w packages/qdadm` to regenerate. */
|
|
3
|
+
export default `core:
|
|
4
|
+
actions:
|
|
5
|
+
save: Save
|
|
6
|
+
cancel: Cancel
|
|
7
|
+
delete: Delete
|
|
8
|
+
edit: Edit
|
|
9
|
+
create: Create
|
|
10
|
+
update: Update
|
|
11
|
+
createAndClose: Create & Close
|
|
12
|
+
updateAndClose: Update & Close
|
|
13
|
+
add: Add
|
|
14
|
+
remove: Remove
|
|
15
|
+
submit: Submit
|
|
16
|
+
reset: Reset
|
|
17
|
+
back: Back
|
|
18
|
+
next: Next
|
|
19
|
+
previous: Previous
|
|
20
|
+
close: Close
|
|
21
|
+
confirm: Confirm
|
|
22
|
+
search: Search
|
|
23
|
+
filter: Filter
|
|
24
|
+
clear: Clear
|
|
25
|
+
refresh: Refresh
|
|
26
|
+
export: Export
|
|
27
|
+
import: Import
|
|
28
|
+
tooltips:
|
|
29
|
+
saveAndContinue: Save and continue editing
|
|
30
|
+
saveAndReturn: Save and return to list
|
|
31
|
+
unsavedChanges: Unsaved changes
|
|
32
|
+
placeholders:
|
|
33
|
+
search: Search…
|
|
34
|
+
noSelection: Click to select…
|
|
35
|
+
fields:
|
|
36
|
+
id: ID
|
|
37
|
+
created_at: Created at
|
|
38
|
+
updated_at: Updated at
|
|
39
|
+
created_by: Created by
|
|
40
|
+
updated_by: Updated by
|
|
41
|
+
messages:
|
|
42
|
+
empty: No items available
|
|
43
|
+
loading: Loading…
|
|
44
|
+
noResults: No results
|
|
45
|
+
noMatching: No matching items
|
|
46
|
+
saved: Saved
|
|
47
|
+
deleted: Deleted
|
|
48
|
+
created: Created
|
|
49
|
+
updated: Updated
|
|
50
|
+
confirmDelete: Are you sure you want to delete this item?
|
|
51
|
+
errors:
|
|
52
|
+
required: Required
|
|
53
|
+
tooShort: Too short (min {min})
|
|
54
|
+
tooLong: Too long (max {max})
|
|
55
|
+
invalid: Invalid value
|
|
56
|
+
notFound: Not found
|
|
57
|
+
unknown: An unknown error occurred
|
|
58
|
+
`
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* GENERATED from core.fr.yml — DO NOT EDIT.
|
|
2
|
+
* Run `npm run gen:i18n-defaults -w packages/qdadm` to regenerate. */
|
|
3
|
+
export default `core:
|
|
4
|
+
actions:
|
|
5
|
+
save: Enregistrer
|
|
6
|
+
cancel: Annuler
|
|
7
|
+
delete: Supprimer
|
|
8
|
+
edit: Modifier
|
|
9
|
+
create: Créer
|
|
10
|
+
update: Mettre à jour
|
|
11
|
+
createAndClose: Créer et fermer
|
|
12
|
+
updateAndClose: Mettre à jour et fermer
|
|
13
|
+
add: Ajouter
|
|
14
|
+
remove: Retirer
|
|
15
|
+
submit: Valider
|
|
16
|
+
reset: Réinitialiser
|
|
17
|
+
back: Retour
|
|
18
|
+
next: Suivant
|
|
19
|
+
previous: Précédent
|
|
20
|
+
close: Fermer
|
|
21
|
+
confirm: Confirmer
|
|
22
|
+
search: Rechercher
|
|
23
|
+
filter: Filtrer
|
|
24
|
+
clear: Effacer
|
|
25
|
+
refresh: Actualiser
|
|
26
|
+
export: Exporter
|
|
27
|
+
import: Importer
|
|
28
|
+
tooltips:
|
|
29
|
+
saveAndContinue: Enregistrer et continuer la modification
|
|
30
|
+
saveAndReturn: Enregistrer et revenir à la liste
|
|
31
|
+
unsavedChanges: Modifications non enregistrées
|
|
32
|
+
placeholders:
|
|
33
|
+
search: Rechercher…
|
|
34
|
+
noSelection: Cliquez pour sélectionner…
|
|
35
|
+
fields:
|
|
36
|
+
id: Identifiant
|
|
37
|
+
created_at: Créé le
|
|
38
|
+
updated_at: Modifié le
|
|
39
|
+
created_by: Créé par
|
|
40
|
+
updated_by: Modifié par
|
|
41
|
+
messages:
|
|
42
|
+
empty: Aucun élément
|
|
43
|
+
loading: Chargement…
|
|
44
|
+
noResults: Aucun résultat
|
|
45
|
+
noMatching: Aucune correspondance
|
|
46
|
+
saved: Enregistré
|
|
47
|
+
deleted: Supprimé
|
|
48
|
+
created: Créé
|
|
49
|
+
updated: Modifié
|
|
50
|
+
confirmDelete: Êtes-vous sûr de vouloir supprimer cet élément ?
|
|
51
|
+
errors:
|
|
52
|
+
required: Requis
|
|
53
|
+
tooShort: Trop court (min {min})
|
|
54
|
+
tooLong: Trop long (max {max})
|
|
55
|
+
invalid: Valeur invalide
|
|
56
|
+
notFound: Introuvable
|
|
57
|
+
unknown: Une erreur inconnue est survenue
|
|
58
|
+
`
|
package/src/i18n/loaders/yaml.ts
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* `LazyLoader` that resolves the YAML text for the requested locale and
|
|
6
6
|
* parses it through the `yaml` package.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* The importer just needs to resolve to `{ default: <yaml text> }`. qdadm's
|
|
9
|
+
* own defaults import generated `.ts` string modules (see DefaultCoreProvider
|
|
10
|
+
* — bundler-portable, no `?raw`), but a consumer on Vite/Vitest can equally
|
|
11
|
+
* pass `?raw` imports of their own YAML:
|
|
9
12
|
*
|
|
10
13
|
* const loader = createYamlLoader({
|
|
11
|
-
* en: () => import('./
|
|
12
|
-
* fr: () => import('./
|
|
14
|
+
* en: () => import('./messages.en.yml?raw'),
|
|
15
|
+
* fr: () => import('./messages.fr.yml?raw'),
|
|
13
16
|
* })
|
|
14
17
|
*
|
|
15
18
|
* The dynamic-import boundary is what gives us code-splitting: the YAML for
|