cabloy 5.1.157 → 5.1.159
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/.cabloy-version +1 -1
- package/.claude/skills/cabloy-contract-loop/SKILL.md +24 -7
- package/.claude/skills/cabloy-contract-loop/evals/evals.json +12 -0
- package/.claude/skills/cabloy-contract-loop/references/contract-loop-map.md +28 -0
- package/.claude/skills/cabloy-contract-loop/references/verification-checklist.md +19 -2
- package/.claude/skills/cabloy-resource-field-update/SKILL.md +18 -0
- package/.claude/skills/cabloy-zova-source-reading/SKILL.md +14 -0
- package/.claude/skills/cabloy-zova-source-reading/references/core-reading-paths.md +10 -0
- package/.github/workflows/docs-pages.yml +18 -2
- package/CHANGELOG.md +26 -0
- package/CONTRIBUTING.md +84 -0
- package/LICENSES.md +5 -0
- package/package.json +2 -1
- package/repo-docs/.vitepress/config.mjs +34 -4
- package/repo-docs/.vitepress/theme/components/GitHubRepositoriesNav.vue +255 -0
- package/repo-docs/.vitepress/theme/custom.css +21 -0
- package/repo-docs/.vitepress/theme/index.js +7 -1
- package/repo-docs/backend/resource-field-update.md +48 -1
- package/repo-docs/blogs/index.md +2 -0
- package/repo-docs/frontend/form-layout-guide.md +6 -6
- package/repo-docs/frontend/page-guide.md +12 -0
- package/repo-docs/frontend/page-meta-guide.md +27 -0
- package/repo-docs/frontend/ssr-architecture-overview.md +4 -0
- package/repo-docs/frontend/ssr-build-deploy-guide.md +1 -1
- package/repo-docs/frontend/zova-form-source-reading-map.md +2 -2
- package/repo-docs/frontend/zova-form-under-the-hood.md +1 -1
- package/repo-docs/frontend/zova-reactivity-under-the-hood.md +65 -9
- package/repo-docs/fullstack/contract-loop-playbook.md +2 -0
- package/repo-docs/fullstack/ssr-site-and-flavor-setup.md +198 -0
- package/repo-docs/fullstack/vona-zova-integration.md +2 -0
- package/repo-e2e/docs/playwright.config.ts +37 -0
- package/repo-e2e/docs/specs/blogs-index.spec.ts +180 -0
- package/repo-e2e/docs/specs/github-repositories-nav.spec.ts +69 -0
- package/repo-e2e/docs/test-results/.last-run.json +4 -0
- package/repo-e2e/specs/cabloy-basic.spec.ts +136 -2
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/entity/{{resourceName}}.tsx_ +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/entity/{{resourceName}}.tsx_ +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +21 -80
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productCreate.tsx +2 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productUpdate.tsx +2 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +2 -0
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordCreate.tsx +1 -0
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectReq.tsx +3 -7
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordUpdate.tsx +1 -0
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +1 -0
- package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +8 -0
- package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +51 -0
- package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +10 -1
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +99 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-web/src/bean/pipe.filter.ts +85 -57
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/cli/templates/rest/render.ts +2 -0
- package/zova/packages-cli/cli-set-front/cli/templates/rest/rest.ts +13 -3
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +31 -11
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +3 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +1 -1
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +68 -8
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +4 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +0 -6
- package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts +16 -9
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +4 -0
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +13 -2
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/schema.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +50 -0
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +60 -7
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/table.ts +6 -4
- package/zova/src/suite-vendor/a-zova/package.json +4 -4
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { nextTick, onMounted, onUnmounted, ref, useId } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
repositories: {
|
|
6
|
+
type: Array,
|
|
7
|
+
required: true,
|
|
8
|
+
},
|
|
9
|
+
screenMenu: Boolean,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const isOpen = ref(false);
|
|
13
|
+
const root = ref();
|
|
14
|
+
const trigger = ref();
|
|
15
|
+
const menu = ref();
|
|
16
|
+
const menuId = useId();
|
|
17
|
+
|
|
18
|
+
function close({ returnFocus = false } = {}) {
|
|
19
|
+
isOpen.value = false;
|
|
20
|
+
if (returnFocus) {
|
|
21
|
+
trigger.value?.focus();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function open({ focusIndex } = {}) {
|
|
26
|
+
isOpen.value = true;
|
|
27
|
+
if (focusIndex === undefined) return;
|
|
28
|
+
|
|
29
|
+
await nextTick();
|
|
30
|
+
menu.value?.querySelectorAll('a')[focusIndex]?.focus();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toggle() {
|
|
34
|
+
if (isOpen.value) {
|
|
35
|
+
close();
|
|
36
|
+
} else {
|
|
37
|
+
open();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleTriggerKeydown(event) {
|
|
42
|
+
if (event.key === 'ArrowDown') {
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
open({ focusIndex: 0 });
|
|
45
|
+
} else if (event.key === 'ArrowUp') {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
open({ focusIndex: props.repositories.length - 1 });
|
|
48
|
+
} else if (event.key === 'Escape') {
|
|
49
|
+
close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function handleMenuKeydown(event) {
|
|
54
|
+
const links = [...menu.value.querySelectorAll('a')];
|
|
55
|
+
const index = links.indexOf(document.activeElement);
|
|
56
|
+
if (index === -1) return;
|
|
57
|
+
|
|
58
|
+
let nextIndex;
|
|
59
|
+
if (event.key === 'ArrowDown') {
|
|
60
|
+
nextIndex = (index + 1) % links.length;
|
|
61
|
+
} else if (event.key === 'ArrowUp') {
|
|
62
|
+
nextIndex = (index - 1 + links.length) % links.length;
|
|
63
|
+
} else if (event.key === 'Home') {
|
|
64
|
+
nextIndex = 0;
|
|
65
|
+
} else if (event.key === 'End') {
|
|
66
|
+
nextIndex = links.length - 1;
|
|
67
|
+
} else if (event.key === 'Escape') {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
close({ returnFocus: true });
|
|
70
|
+
return;
|
|
71
|
+
} else {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
links[nextIndex]?.focus();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handlePointerDown(event) {
|
|
80
|
+
if (isOpen.value && !root.value?.contains(event.target)) {
|
|
81
|
+
close();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleFocusIn(event) {
|
|
86
|
+
if (isOpen.value && !root.value?.contains(event.target)) {
|
|
87
|
+
close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onMounted(() => {
|
|
92
|
+
document.addEventListener('pointerdown', handlePointerDown);
|
|
93
|
+
document.addEventListener('focusin', handleFocusIn);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
onUnmounted(() => {
|
|
97
|
+
document.removeEventListener('pointerdown', handlePointerDown);
|
|
98
|
+
document.removeEventListener('focusin', handleFocusIn);
|
|
99
|
+
});
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<template>
|
|
103
|
+
<section v-if="screenMenu" class="cabloy-github-repositories cabloy-github-repositories--screen">
|
|
104
|
+
<p class="cabloy-github-repositories__title">GitHub repositories</p>
|
|
105
|
+
<a
|
|
106
|
+
v-for="repository in repositories"
|
|
107
|
+
:key="repository.link"
|
|
108
|
+
class="cabloy-github-repositories__screen-link"
|
|
109
|
+
:href="repository.link"
|
|
110
|
+
target="_blank"
|
|
111
|
+
rel="noopener noreferrer"
|
|
112
|
+
>
|
|
113
|
+
{{ repository.text }}
|
|
114
|
+
</a>
|
|
115
|
+
</section>
|
|
116
|
+
|
|
117
|
+
<div v-else ref="root" class="cabloy-github-repositories">
|
|
118
|
+
<button
|
|
119
|
+
ref="trigger"
|
|
120
|
+
class="cabloy-github-repositories__trigger"
|
|
121
|
+
type="button"
|
|
122
|
+
aria-label="GitHub repositories"
|
|
123
|
+
aria-haspopup="menu"
|
|
124
|
+
:aria-controls="menuId"
|
|
125
|
+
:aria-expanded="isOpen"
|
|
126
|
+
@click="toggle"
|
|
127
|
+
@keydown="handleTriggerKeydown"
|
|
128
|
+
>
|
|
129
|
+
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
|
130
|
+
<path
|
|
131
|
+
fill="currentColor"
|
|
132
|
+
d="M12 2C6.477 2 2 6.59 2 12.253c0 4.53 2.865 8.37 6.839 9.727.5.095.682-.223.682-.494 0-.244-.009-1.05-.014-1.906-2.782.621-3.369-1.215-3.369-1.215-.455-1.185-1.11-1.5-1.11-1.5-.908-.64.069-.627.069-.627 1.004.073 1.532 1.057 1.532 1.057.892 1.567 2.341 1.114 2.91.852.09-.663.349-1.115.635-1.371-2.22-.26-4.555-1.139-4.555-5.067 0-1.12.39-2.034 1.03-2.752-.104-.26-.447-1.306.098-2.724 0 0 .84-.278 2.75 1.051A9.3 9.3 0 0 1 12 6.89a9.3 9.3 0 0 1 2.504.347c1.909-1.33 2.748-1.051 2.748-1.051.546 1.418.203 2.464.1 2.724.64.718 1.028 1.632 1.028 2.752 0 3.938-2.339 4.804-4.566 5.058.359.32.678.947.678 1.91 0 1.38-.012 2.492-.012 2.832 0 .274.18.594.688.493C19.14 20.619 22 16.78 22 12.253 22 6.59 17.523 2 12 2Z"
|
|
133
|
+
/>
|
|
134
|
+
</svg>
|
|
135
|
+
</button>
|
|
136
|
+
|
|
137
|
+
<div
|
|
138
|
+
v-if="isOpen"
|
|
139
|
+
:id="menuId"
|
|
140
|
+
ref="menu"
|
|
141
|
+
class="cabloy-github-repositories__menu"
|
|
142
|
+
role="menu"
|
|
143
|
+
aria-label="GitHub repositories"
|
|
144
|
+
@keydown="handleMenuKeydown"
|
|
145
|
+
>
|
|
146
|
+
<a
|
|
147
|
+
v-for="repository in repositories"
|
|
148
|
+
:key="repository.link"
|
|
149
|
+
class="cabloy-github-repositories__menu-link"
|
|
150
|
+
:href="repository.link"
|
|
151
|
+
role="menuitem"
|
|
152
|
+
target="_blank"
|
|
153
|
+
rel="noopener noreferrer"
|
|
154
|
+
@click="close"
|
|
155
|
+
>
|
|
156
|
+
{{ repository.text }}
|
|
157
|
+
</a>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</template>
|
|
161
|
+
|
|
162
|
+
<style scoped>
|
|
163
|
+
.cabloy-github-repositories {
|
|
164
|
+
position: relative;
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.cabloy-github-repositories__trigger {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
justify-content: center;
|
|
173
|
+
width: 32px;
|
|
174
|
+
height: 32px;
|
|
175
|
+
border: 0;
|
|
176
|
+
border-radius: 6px;
|
|
177
|
+
background: transparent;
|
|
178
|
+
color: var(--vp-c-text-2);
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.cabloy-github-repositories__trigger:hover,
|
|
183
|
+
.cabloy-github-repositories__trigger[aria-expanded='true'] {
|
|
184
|
+
color: var(--vp-c-text-1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.cabloy-github-repositories__trigger:focus-visible,
|
|
188
|
+
.cabloy-github-repositories__menu-link:focus-visible,
|
|
189
|
+
.cabloy-github-repositories__screen-link:focus-visible {
|
|
190
|
+
outline: 2px solid var(--vp-c-brand-1);
|
|
191
|
+
outline-offset: 2px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.cabloy-github-repositories__trigger svg {
|
|
195
|
+
width: 20px;
|
|
196
|
+
height: 20px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.cabloy-github-repositories__menu {
|
|
200
|
+
position: absolute;
|
|
201
|
+
z-index: 1;
|
|
202
|
+
top: calc(100% + 4px);
|
|
203
|
+
right: 0;
|
|
204
|
+
min-width: 160px;
|
|
205
|
+
border: 1px solid var(--vp-c-divider);
|
|
206
|
+
border-radius: 8px;
|
|
207
|
+
padding: 4px;
|
|
208
|
+
background: var(--vp-c-bg-elv);
|
|
209
|
+
box-shadow: var(--vp-shadow-3);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.cabloy-github-repositories__menu-link,
|
|
213
|
+
.cabloy-github-repositories__screen-link {
|
|
214
|
+
display: block;
|
|
215
|
+
color: var(--vp-c-text-1);
|
|
216
|
+
text-decoration: none;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.cabloy-github-repositories__menu-link {
|
|
220
|
+
border-radius: 4px;
|
|
221
|
+
padding: 0.5rem 0.75rem;
|
|
222
|
+
font-size: 0.875rem;
|
|
223
|
+
line-height: 1.25rem;
|
|
224
|
+
white-space: nowrap;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.cabloy-github-repositories__menu-link:hover {
|
|
228
|
+
background: var(--vp-c-bg-soft);
|
|
229
|
+
color: var(--vp-c-brand-1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.cabloy-github-repositories--screen {
|
|
233
|
+
display: block;
|
|
234
|
+
padding: 12px 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.cabloy-github-repositories__title {
|
|
238
|
+
margin: 0 0 4px;
|
|
239
|
+
color: var(--vp-c-text-2);
|
|
240
|
+
font-size: 0.75rem;
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
line-height: 1.25rem;
|
|
243
|
+
text-transform: uppercase;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.cabloy-github-repositories__screen-link {
|
|
247
|
+
padding: 0.5rem 0;
|
|
248
|
+
font-size: 1rem;
|
|
249
|
+
line-height: 1.5rem;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.cabloy-github-repositories__screen-link:hover {
|
|
253
|
+
color: var(--vp-c-brand-1);
|
|
254
|
+
}
|
|
255
|
+
</style>
|
|
@@ -63,6 +63,27 @@
|
|
|
63
63
|
color: #fdba74;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
@media (min-width: 960px) {
|
|
67
|
+
.cabloy-blogs-index .VPDoc:not(.has-sidebar) .container,
|
|
68
|
+
.cabloy-blogs-index .VPDoc:not(.has-sidebar) .content {
|
|
69
|
+
max-width: 1440px;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@media (min-width: 1280px) {
|
|
74
|
+
.cabloy-blogs-article .VPDoc.has-aside:not(.has-sidebar) .container {
|
|
75
|
+
max-width: 1216px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.cabloy-blogs-article .VPDoc.has-aside:not(.has-sidebar) .content {
|
|
79
|
+
max-width: 960px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.cabloy-blogs-article .VPDoc.has-aside:not(.has-sidebar) .content-container {
|
|
83
|
+
max-width: 896px;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
.cabloy-blog-grid {
|
|
67
88
|
display: grid;
|
|
68
89
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import DefaultTheme from 'vitepress/theme';
|
|
2
2
|
|
|
3
|
+
import GitHubRepositoriesNav from './components/GitHubRepositoriesNav.vue';
|
|
3
4
|
import './custom.css';
|
|
4
5
|
|
|
5
|
-
export default
|
|
6
|
+
export default {
|
|
7
|
+
extends: DefaultTheme,
|
|
8
|
+
enhanceApp({ app }) {
|
|
9
|
+
app.component('GitHubRepositoriesNav', GitHubRepositoriesNav);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -13,6 +13,7 @@ Use it when you need to:
|
|
|
13
13
|
- add a stored field to an existing resource;
|
|
14
14
|
- refine validation, titles, OpenAPI metadata, or `ZovaRender.field(...)` / `ZovaRender.cell(...)` metadata for an existing field;
|
|
15
15
|
- add enum-like field constraints;
|
|
16
|
+
- filter or sort an existing foreign-key field by a display column from its related table without replacing the stored foreign-key contract;
|
|
16
17
|
- decide whether a persisted field change increments `vonaModule.fileVersion`;
|
|
17
18
|
- add a custom form-field or table-cell renderer because shared renderer options are insufficient.
|
|
18
19
|
|
|
@@ -93,6 +94,43 @@ For projected fields, use `$makeMetadata(...)` for metadata-only refinement and
|
|
|
93
94
|
|
|
94
95
|
Read [DTO Infer and Generation](/backend/dto-infer-generation) when inference cannot express the intended contract. If serialization metadata controls the returned value, also confirm that the target controller action opts into `@Core.serializer()`.
|
|
95
96
|
|
|
97
|
+
## Filter and sort a relation by its display field
|
|
98
|
+
|
|
99
|
+
A resource list can search and order a relation by a human-readable column from its related table without changing the persisted relation contract. Keep the entity field as the foreign-key identity and place the join mapping on that entity field. For example, `studentId` remains a `TableIdentity` even though the list filter accepts part of the student's name:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
@Api.field(
|
|
103
|
+
v.filter({
|
|
104
|
+
table: 'trainingStudent',
|
|
105
|
+
joinType: 'innerJoin',
|
|
106
|
+
joinOn: ['studentId', 'trainingStudent.id'],
|
|
107
|
+
originalName: 'name',
|
|
108
|
+
op: '_includesI_',
|
|
109
|
+
}),
|
|
110
|
+
ZovaRender.column({ enableSorting: true }),
|
|
111
|
+
v.tableIdentity(),
|
|
112
|
+
)
|
|
113
|
+
studentId: TableIdentity;
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`table` and `joinOn` identify the relation, while `originalName` resolves both the `studentId` filter and order key to `trainingStudent.name`. `_includesI_` makes the filter a case-insensitive partial-name match. `ZovaRender.column({ enableSorting: true })` exposes the sortable table column; the query pipeline uses the same field metadata to add the join and rewrite `orders: [['studentId', 'asc']]` to the related display column.
|
|
117
|
+
|
|
118
|
+
Use `innerJoin` deliberately when the relation is required and unmatched rows should not participate in this filter/order path. For an optional relation whose unmatched rows must remain visible, choose the join behavior explicitly instead of copying the required-relation example.
|
|
119
|
+
|
|
120
|
+
Keep `studentId` as the public filter and order key. In the select request DTO, override only its query-input schema and renderer so a text fragment reaches the join-backed filter; the entity and create/update contracts remain foreign-key identities:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
fields: {
|
|
124
|
+
studentId: $makeSchema(
|
|
125
|
+
ZovaRender.field('basic-input:formFieldInput'),
|
|
126
|
+
v.optional(),
|
|
127
|
+
z.string(),
|
|
128
|
+
),
|
|
129
|
+
},
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Do not add a parallel `studentName` query field unless the API intentionally needs distinct ID and display-name filtering semantics. For the underlying query behavior, see [ORM Select Guide](/backend/orm-select-guide). For the list-page query path, see [Filter to Query to Select Data Flow](/frontend/filter-query-select-data-flow-guide).
|
|
133
|
+
|
|
96
134
|
## Choose the renderer branch
|
|
97
135
|
|
|
98
136
|
Prefer the smallest rendering change that expresses the requirement:
|
|
@@ -141,6 +179,14 @@ Minimum backend coverage generally includes:
|
|
|
141
179
|
- get-by-id or view response;
|
|
142
180
|
- delete behavior when it is relevant.
|
|
143
181
|
|
|
182
|
+
For join-backed relation filtering and sorting, also verify:
|
|
183
|
+
|
|
184
|
+
- the projected request field is optional text while the entity remains a foreign-key identity;
|
|
185
|
+
- direct DTO metadata retains the intended `table`, `joinType`, `joinOn`, `originalName`, and operator;
|
|
186
|
+
- the select action exposes the intended query parameter without an unintended parallel display-name parameter;
|
|
187
|
+
- filtering returns records whose related display value matches the text fragment;
|
|
188
|
+
- ascending and descending orders return the expected related-display ordering, including the selected join behavior for unmatched rows.
|
|
189
|
+
|
|
144
190
|
For constrained enum-like values, add a negative test that proves an invalid value is rejected. Test-local persisted resources must be deleted in `finally` using precise owned identities and reverse dependency order.
|
|
145
191
|
|
|
146
192
|
## Verification checklist
|
|
@@ -151,6 +197,7 @@ Choose checks that match the layers changed:
|
|
|
151
197
|
- run the narrow resource test and relevant typecheck;
|
|
152
198
|
- run `npm run test` for any `meta.version.ts` change so the test database is recreated and migration consistency is exercised;
|
|
153
199
|
- run the relevant frontend metadata/build/dependency synchronization when custom renderer resources are involved;
|
|
154
|
-
- verify action-level serializer behavior with an API test when `v.serializer*` metadata changes returned fields
|
|
200
|
+
- verify action-level serializer behavior with an API test when `v.serializer*` metadata changes returned fields;
|
|
201
|
+
- for join-backed filters and sorting, inspect transformed `where`, `orders`, and `joins`, then exercise the resource endpoint so metadata-only success cannot hide incorrect query behavior.
|
|
155
202
|
|
|
156
203
|
Finish by confirming the backend contract, frontend resources, generated handoff, and user-visible locale labels all describe the same field behavior.
|
package/repo-docs/blogs/index.md
CHANGED
|
@@ -219,11 +219,11 @@ Set `layout: 'flow'` when compact fields should appear from left to right withou
|
|
|
219
219
|
|
|
220
220
|
## How the resolver handles the declared tree
|
|
221
221
|
|
|
222
|
-
Before rendering, `resolveFormLayout(...)` reconciles `formLayout` with the current scene's resolved schema properties.
|
|
222
|
+
Before rendering, `resolveFormLayout(...)` reconciles `formLayout` with the current scene's resolved schema properties. A configured Form Layout is an **explicit allow-list**: it renders only successfully resolved declared fields and explicit block nodes. The preceding OpenAPI normalization step is explained in [OpenAPI Runtime Under the Hood](/frontend/a-openapi-under-the-hood#scene-overlays-fieldsource-and-preserved-aliases).
|
|
223
223
|
|
|
224
|
-
### Eligible and
|
|
224
|
+
### Eligible and declared fields
|
|
225
225
|
|
|
226
|
-
Only schema properties with `rest.visible !== false` are eligible
|
|
226
|
+
Only schema properties with `rest.visible !== false` are eligible for a field declaration. Eligibility alone does not render a field: a visible field absent from `formLayout.children` does not render and produces no diagnostic.
|
|
227
227
|
|
|
228
228
|
A field that uses `fieldSource` is represented at runtime by its nested canonical source key. The loader stores that source path in `key`, retains the first differing original schema name in `schemaKey`, and retains further coalesced names in `schemaKeys`. A property already named by its canonical key retains that identity in `key`. These preserved names are aliases for declaration matching, not separate fields or bindings.
|
|
229
229
|
|
|
@@ -237,9 +237,9 @@ For example, a real relation declaration `studentContentForm` can resolve to the
|
|
|
237
237
|
|
|
238
238
|
An alias or relation prefix must resolve to exactly one eligible canonical source. If multiple visible source keys match, it is unresolved and receives `unknownField`; declare the exact canonical source key instead. Exact canonical matches win over colliding aliases. Invisible properties contribute neither eligible fields nor usable aliases.
|
|
239
239
|
|
|
240
|
-
Duplicate declarations are detected by canonical key, so an alias and `content.descriptionMarkdown` cannot render the same field twice. Resolved field names, duplicate identity, and tab paths all use the canonical key. If a declaration is removed because it is unknown, invisible, ambiguous, or duplicate,
|
|
240
|
+
Duplicate declarations are detected by canonical key, so an alias and `content.descriptionMarkdown` cannot render the same field twice. Resolved field names, duplicate identity, and tab paths all use the canonical key. If a declaration is removed because it is unknown, invisible, ambiguous, or duplicate, it does not render and no other eligible fields are added as a fallback.
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
Use `rest.visible: false` when a field must be ineligible in the schema scene. In a configured Form Layout, leave a visible field out of `formLayout.children` when it should not render there.
|
|
243
243
|
|
|
244
244
|
### Invalid declarations and diagnostics
|
|
245
245
|
|
|
@@ -384,7 +384,7 @@ Here `formFieldLayout.inline: true` controls how each field wrapper is presented
|
|
|
384
384
|
5. Use `layout`, `formFieldLayout`, `options`, or provider behaviors when the requirement is one field's wrapper or renderer.
|
|
385
385
|
6. Keep entry actions in page-entry toolbar blocks. Keep filter action semantics in `basic-page:blockFilterActions`; place that block inside Form Layout when the actions must share structural Grid or flow placement with fields.
|
|
386
386
|
7. For maintained Cabloy Basic list filters, prefer one inline flow section that explicitly lists every real filter-schema field in schema order and ends with one embedded `basic-page:blockFilterActions` block. Do not add synthetic request-only filter fields, alter filter transforms, or combine it with a sibling action block.
|
|
387
|
-
8. Review field names against the scene-specific schema.
|
|
387
|
+
8. Review field names against the scene-specific schema. Explicitly list every field that should render; omitted visible fields do not render, while unknown and duplicate declarations are silently pruned from the rendered plan.
|
|
388
388
|
9. Prefer an exact canonical source path when an alias or relation prefix could match multiple visible fields. Do not list both an alias and its canonical key; canonical duplicate detection keeps only the first declaration.
|
|
389
389
|
|
|
390
390
|
## Source-reading and verification path
|
|
@@ -66,6 +66,18 @@ class ControllerPageCounter {
|
|
|
66
66
|
}
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
### Keep callbacks on the reactive bean path
|
|
70
|
+
|
|
71
|
+
Use ordinary controller methods as the default for actions that change controller state. Do not expose a state-mutating class-field arrow callback such as `onIncrement = () => this.count++`: it captures construction-time `this` before Zova exposes the controller's reactive/proxied bean.
|
|
72
|
+
|
|
73
|
+
At a TSX or component-prop boundary, forward to the method instead:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
onClick={() => this.increment()}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When an external API requires a stable callback reference, create the closure in `__init__()` after the controller has been prepared. See [Zova Reactivity Under the Hood](/frontend/zova-reactivity-under-the-hood#construction-time-this-and-the-exposed-reactive-bean) for the lifecycle reason and safe pattern.
|
|
80
|
+
|
|
69
81
|
## Add render logic
|
|
70
82
|
|
|
71
83
|
Representative TSX render pattern:
|
|
@@ -90,6 +90,33 @@ export interface IPageMeta {
|
|
|
90
90
|
|
|
91
91
|
In the current Basic source, this is typically what the Admin layout uses for the level-2 item label instead of the broader menu-backed workspace title.
|
|
92
92
|
|
|
93
|
+
### Resource entry `pageTitleKey`
|
|
94
|
+
|
|
95
|
+
For schema-driven Resource entry pages, `basic-pageentry:blockPageEntry` derives `pageTitle` from the current form data. Its default `pageTitleKey` is `name`.
|
|
96
|
+
|
|
97
|
+
When the resource's human-readable identifier uses another top-level field, configure that field explicitly on the page-entry block in the DTO:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
@Dto({
|
|
101
|
+
blocks: [
|
|
102
|
+
ZovaRender.block('basic-pageentry:blockPageEntry', {
|
|
103
|
+
pageTitleKey: 'title',
|
|
104
|
+
blocks: [
|
|
105
|
+
// form and toolbar blocks
|
|
106
|
+
],
|
|
107
|
+
}),
|
|
108
|
+
],
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Use the same key in the create, update, and view DTOs when those scenes should show a consistent task title. Common choices include `title` and `code`; do not add a synthetic `name` field merely to satisfy the default.
|
|
113
|
+
|
|
114
|
+
Keep these boundaries in mind:
|
|
115
|
+
|
|
116
|
+
- `pageTitleKey` controls the routed-shell level-2 task title, not the resource or OpenAPI schema title and not the browser document title.
|
|
117
|
+
- The current implementation reads one exact top-level form-data key. It is not a dotted-path resolver or a formatter.
|
|
118
|
+
- When the source field is `null` or `undefined`, the current page-entry runtime falls back to the resolved form DTO schema's OpenAPI `title`. Give the create DTO an operation-specific title such as `openapi: { title: $locale('CreateProduct') }` when it needs an initial task label; a populated source field takes precedence. The fallback is not used for an empty string.
|
|
119
|
+
|
|
93
120
|
### `pageDirty`
|
|
94
121
|
|
|
95
122
|
`pageDirty` tells the routed shell whether the current work item should appear dirty.
|
|
@@ -187,6 +187,10 @@ What can differ by edition is usually:
|
|
|
187
187
|
|
|
188
188
|
So the architecture model is shared, while some concrete frontend examples remain edition-sensitive.
|
|
189
189
|
|
|
190
|
+
## Adding an independent SSR site
|
|
191
|
+
|
|
192
|
+
The architecture maps one Vona SSR site to one built Zova flavor bundle. When a new deployable mount needs its own flavor and Vona dispatch registration, follow [Independent SSR Site and Flavor Setup](/fullstack/ssr-site-and-flavor-setup). It covers the matching site ID, public path, bundle path, generated REST types, paired build wrapper, and hydration proof.
|
|
193
|
+
|
|
190
194
|
## Recommended reading order
|
|
191
195
|
|
|
192
196
|
Use this order when you need the shortest path from mental model to implementation detail:
|
|
@@ -130,7 +130,7 @@ If you need deeper script control or need to verify the exact Zova flavor path,
|
|
|
130
130
|
|
|
131
131
|
### Build frontend SSR output only
|
|
132
132
|
|
|
133
|
-
Use this when you need the frontend SSR artifacts refreshed but do not yet need the full backend build flow.
|
|
133
|
+
Use this when you need the frontend SSR artifacts refreshed but do not yet need the full backend build flow. If the task creates a new independently mounted site rather than refreshing an existing one, first follow [Independent SSR Site and Flavor Setup](/fullstack/ssr-site-and-flavor-setup): the new site needs its own source-confirmed wrapper that pairs its SSR and REST outputs, not a renamed default Admin or Web wrapper.
|
|
134
134
|
|
|
135
135
|
Representative current Basic command:
|
|
136
136
|
|
|
@@ -233,7 +233,7 @@ Use this path when you are asking questions like:
|
|
|
233
233
|
- where does `formLayout` come from in a resource DTO?
|
|
234
234
|
- how are fields, embedded blocks, sections, groups, and tabs normalized before rendering?
|
|
235
235
|
- how do canonical keys, preserved schema aliases, and unique relation-prefix shorthand resolve to one field?
|
|
236
|
-
- why
|
|
236
|
+
- why do omitted fields not render, or duplicate fields get removed?
|
|
237
237
|
- where does Cabloy Basic render responsive grids and tab error badges?
|
|
238
238
|
|
|
239
239
|
### Read the docs first
|
|
@@ -266,7 +266,7 @@ Use this path when you are asking questions like:
|
|
|
266
266
|
- the resolver filters visible fields, resolves exact canonical keys before unique aliases and unique prefixes, and records duplicate identity and tab paths by canonical key
|
|
267
267
|
- the Basic block controller renders sections, groups, tabs, field spans, and embedded blocks while delegating canonical field names to `$$form.renderField(...)`
|
|
268
268
|
- `blockFilterActions` shows how a block rendered inside Form Layout reuses the inherited form CEL scope to invoke `$$filter`
|
|
269
|
-
- the OpenAPI and Form Layout unit tests verify canonicalization, alias precedence, ambiguity, duplicates, visibility,
|
|
269
|
+
- the OpenAPI and Form Layout unit tests verify canonicalization, explicit field inclusion, alias precedence, ambiguity, duplicates, visibility, and tab paths
|
|
270
270
|
- the Student test verifies emitted metadata nesting, columns, spans, embedded action blocks, and optional IDs; it is not a browser rendering test
|
|
271
271
|
|
|
272
272
|
## 8. Resource-driven CRUD page integration
|
|
@@ -454,7 +454,7 @@ That means automatic schema-driven rendering is not happening magically in the w
|
|
|
454
454
|
|
|
455
455
|
When `ZForm` receives a nonempty block list, the render bean delegates body rendering to those blocks instead of iterating schema fields directly. For Cabloy Basic structural forms, `basic-form:blockFormLayout` resolves `formLayout` against the form's current schema properties and calls `$$form.renderField(...)` for each surviving layout field.
|
|
456
456
|
|
|
457
|
-
For a field declaration, the shared resolver first accepts an exact canonical key, then a uniquely mapped preserved schema alias, and then a unique relation-prefix shorthand. It rewrites an alias or shorthand to the canonical key before calling `$$form.renderField(...)`. Exact canonical keys win over colliding aliases; ambiguous aliases or prefixes are reported as `unknownField`.
|
|
457
|
+
For a field declaration, the shared resolver first accepts an exact canonical key, then a uniquely mapped preserved schema alias, and then a unique relation-prefix shorthand. It rewrites an alias or shorthand to the canonical key before calling `$$form.renderField(...)`. Exact canonical keys win over colliding aliases; ambiguous aliases or prefixes are reported as `unknownField`. A configured Form Layout is an explicit allow-list: only fields represented by surviving declarations render. Duplicate tracking and tab-path bookkeeping use the canonical key. See [Form Layout Guide](/frontend/form-layout-guide#how-the-resolver-handles-the-declared-tree) for the full DTO authoring rules.
|
|
458
458
|
|
|
459
459
|
Form Layout also supports a leaf `block` node. It wraps an existing resource block descriptor and the Basic renderer invokes it with the inherited `IJsxRenderContextForm`, including the same JSX runtime and CEL scope. The node has no schema property or field value; for example, a filter can place `basic-page:blockFilterActions` inside a flow section while that action block continues to read `$$filter` from the filter-owned form scope.
|
|
460
460
|
|
|
@@ -151,6 +151,62 @@ This is one of the most important source-level facts for understanding Zova.
|
|
|
151
151
|
|
|
152
152
|
Zova does not require the business author to write `reactive({ ... })` around controller state, because the framework already treats the controller bean as the reactive object.
|
|
153
153
|
|
|
154
|
+
#### Construction-time `this` and the exposed reactive bean
|
|
155
|
+
|
|
156
|
+
There is one lifecycle boundary that matters when a callback closes over `this`:
|
|
157
|
+
|
|
158
|
+
1. the container first constructs the raw class instance with `new BeanClass(...)`;
|
|
159
|
+
2. it then exposes the framework-managed reactive/proxied bean; and
|
|
160
|
+
3. it invokes `__init__()` only after that preparation completes.
|
|
161
|
+
|
|
162
|
+
That distinction is normally invisible when a controller uses ordinary methods and render-time `this.member` access. It matters for a class-field arrow callback that mutates controller state, because the arrow is created during construction and lexically captures the construction-time raw `this`:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
class ControllerPageCounter {
|
|
166
|
+
count = 0;
|
|
167
|
+
|
|
168
|
+
// Avoid for a state-mutating controller callback.
|
|
169
|
+
onIncrement = () => {
|
|
170
|
+
this.count++;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The field can change on the raw instance, but the mutation bypasses the reactive/proxied bean through which render dependencies were collected. Vue therefore receives no invalidation notification. A later update to another dependency can rerender the component and reveal the changed underlying value, which can make this look intermittent.
|
|
176
|
+
|
|
177
|
+
Prefer an ordinary controller method for a normal TSX action:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
class ControllerPageCounter {
|
|
181
|
+
count = 0;
|
|
182
|
+
|
|
183
|
+
increment() {
|
|
184
|
+
this.count++;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
protected render() {
|
|
188
|
+
return <button onClick={() => this.increment()}>Increment</button>;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
If an external API requires a stable stored callback, create that closure in `__init__()` instead. At that point, its lexical `this` is the framework-exposed reactive/proxied bean:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
class ControllerPageCounter {
|
|
197
|
+
count = 0;
|
|
198
|
+
onIncrement: () => void;
|
|
199
|
+
|
|
200
|
+
protected async __init__() {
|
|
201
|
+
this.onIncrement = () => {
|
|
202
|
+
this.count++;
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
This is not a rule that arrow functions are generally non-reactive. The narrow hazard is a **class-field arrow callback that captures construction-time `this` and mutates bean state**.
|
|
209
|
+
|
|
154
210
|
### 4. `$computed()` is an instance-scoped wrapper around Vue `computed(...)`
|
|
155
211
|
|
|
156
212
|
In:
|
|
@@ -238,21 +294,21 @@ A practical reading takeaway is:
|
|
|
238
294
|
|
|
239
295
|
### 7. Field mutation becomes normal reactive invalidation and rerender
|
|
240
296
|
|
|
241
|
-
Once
|
|
297
|
+
Once render has read fields through the framework-exposed reactive/proxied controller, a mutation through that same identity, such as:
|
|
242
298
|
|
|
243
299
|
```typescript
|
|
244
300
|
this.count++;
|
|
245
301
|
```
|
|
246
302
|
|
|
247
|
-
|
|
303
|
+
behaves the way a Vue reader would expect at the reactive-engine level:
|
|
248
304
|
|
|
249
305
|
- the field change invalidates dependencies
|
|
250
306
|
- computed values depending on that field are recomputed
|
|
251
307
|
- the next render sees the updated values
|
|
252
308
|
|
|
253
|
-
|
|
309
|
+
The construction-time class-field arrow case described above is the exception: its raw `this` can change the underlying field without triggering the dependency that render collected through the exposed bean.
|
|
254
310
|
|
|
255
|
-
The architectural surface is what changed.
|
|
311
|
+
So the runtime behavior is still recognizably Vue-like. The architectural surface, including the bean lifecycle and identity boundary, is what changed.
|
|
256
312
|
|
|
257
313
|
## A compact call-flow sketch
|
|
258
314
|
|
|
@@ -260,13 +316,13 @@ The architectural surface is what changed.
|
|
|
260
316
|
useControllerPage(...)
|
|
261
317
|
-> _useController(...)
|
|
262
318
|
-> ctx.bean._newBeanInner(..., markReactive = true)
|
|
263
|
-
-> BeanContainer
|
|
264
|
-
-> BeanContainer
|
|
265
|
-
-> controller __init__ wires $computed / $watch helpers
|
|
319
|
+
-> BeanContainer constructs the raw controller bean
|
|
320
|
+
-> BeanContainer exposes the reactive/proxied controller bean
|
|
321
|
+
-> controller __init__ wires $computed / $watch helpers and any stored callbacks
|
|
266
322
|
-> component render is patched toward controller/render bean
|
|
267
323
|
-> render-time controller data update refreshes page route data
|
|
268
|
-
-> render reads
|
|
269
|
-
->
|
|
324
|
+
-> render reads fields through the exposed bean
|
|
325
|
+
-> mutation through that same identity invalidates dependencies
|
|
270
326
|
-> rerender produces updated UI
|
|
271
327
|
```
|
|
272
328
|
|
|
@@ -146,6 +146,8 @@ Then use the **reverse chain**:
|
|
|
146
146
|
|
|
147
147
|
See [Frontend Metadata Back to Backend](/fullstack/frontend-metadata-to-backend) for the end-to-end reverse-chain bridge from frontend-owned truth to backend-visible shared handoff.
|
|
148
148
|
|
|
149
|
+
When the frontend change creates a new independently mounted SSR site rather than refreshing an existing consumer, use [Independent SSR Site and Flavor Setup](/fullstack/ssr-site-and-flavor-setup). The new Vona consumer needs a matched flavor, paired SSR/REST build wrapper, site registration, and dispatch/hydration proof.
|
|
150
|
+
|
|
149
151
|
### 3. Do generated artifacts look stale?
|
|
150
152
|
|
|
151
153
|
Examples:
|