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.
Files changed (90) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-contract-loop/SKILL.md +24 -7
  3. package/.claude/skills/cabloy-contract-loop/evals/evals.json +12 -0
  4. package/.claude/skills/cabloy-contract-loop/references/contract-loop-map.md +28 -0
  5. package/.claude/skills/cabloy-contract-loop/references/verification-checklist.md +19 -2
  6. package/.claude/skills/cabloy-resource-field-update/SKILL.md +18 -0
  7. package/.claude/skills/cabloy-zova-source-reading/SKILL.md +14 -0
  8. package/.claude/skills/cabloy-zova-source-reading/references/core-reading-paths.md +10 -0
  9. package/.github/workflows/docs-pages.yml +18 -2
  10. package/CHANGELOG.md +26 -0
  11. package/CONTRIBUTING.md +84 -0
  12. package/LICENSES.md +5 -0
  13. package/package.json +2 -1
  14. package/repo-docs/.vitepress/config.mjs +34 -4
  15. package/repo-docs/.vitepress/theme/components/GitHubRepositoriesNav.vue +255 -0
  16. package/repo-docs/.vitepress/theme/custom.css +21 -0
  17. package/repo-docs/.vitepress/theme/index.js +7 -1
  18. package/repo-docs/backend/resource-field-update.md +48 -1
  19. package/repo-docs/blogs/index.md +2 -0
  20. package/repo-docs/frontend/form-layout-guide.md +6 -6
  21. package/repo-docs/frontend/page-guide.md +12 -0
  22. package/repo-docs/frontend/page-meta-guide.md +27 -0
  23. package/repo-docs/frontend/ssr-architecture-overview.md +4 -0
  24. package/repo-docs/frontend/ssr-build-deploy-guide.md +1 -1
  25. package/repo-docs/frontend/zova-form-source-reading-map.md +2 -2
  26. package/repo-docs/frontend/zova-form-under-the-hood.md +1 -1
  27. package/repo-docs/frontend/zova-reactivity-under-the-hood.md +65 -9
  28. package/repo-docs/fullstack/contract-loop-playbook.md +2 -0
  29. package/repo-docs/fullstack/ssr-site-and-flavor-setup.md +198 -0
  30. package/repo-docs/fullstack/vona-zova-integration.md +2 -0
  31. package/repo-e2e/docs/playwright.config.ts +37 -0
  32. package/repo-e2e/docs/specs/blogs-index.spec.ts +180 -0
  33. package/repo-e2e/docs/specs/github-repositories-nav.spec.ts +69 -0
  34. package/repo-e2e/docs/test-results/.last-run.json +4 -0
  35. package/repo-e2e/specs/cabloy-basic.spec.ts +136 -2
  36. package/vona/packages-cli/cli/package.json +1 -1
  37. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/entity/{{resourceName}}.tsx_ +1 -1
  38. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/entity/{{resourceName}}.tsx_ +1 -1
  39. package/vona/packages-cli/cli-set-api/package.json +1 -1
  40. package/vona/packages-vona/vona/package.json +1 -1
  41. package/vona/pnpm-lock.yaml +21 -80
  42. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productCreate.tsx +2 -0
  45. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productUpdate.tsx +2 -0
  46. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +2 -0
  47. package/vona/src/suite/a-training/modules/training-record/src/dto/recordCreate.tsx +1 -0
  48. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectReq.tsx +3 -7
  49. package/vona/src/suite/a-training/modules/training-record/src/dto/recordUpdate.tsx +1 -0
  50. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +1 -0
  51. package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +8 -0
  52. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +51 -0
  53. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +3 -0
  54. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +3 -0
  55. package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +1 -0
  56. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +1 -0
  57. package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +1 -0
  58. package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +1 -0
  59. package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +10 -1
  60. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +99 -1
  61. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  62. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  63. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  64. package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
  65. package/vona/src/suite-vendor/a-vona/modules/a-web/src/bean/pipe.filter.ts +85 -57
  66. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  67. package/zova/packages-cli/cli/package.json +2 -2
  68. package/zova/packages-cli/cli-set-front/cli/templates/rest/render.ts +2 -0
  69. package/zova/packages-cli/cli-set-front/cli/templates/rest/rest.ts +13 -3
  70. package/zova/packages-cli/cli-set-front/package.json +1 -1
  71. package/zova/packages-zova/zova/package.json +2 -2
  72. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +31 -11
  73. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +3 -0
  74. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +1 -1
  75. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +68 -8
  76. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  77. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +4 -0
  78. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  79. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  80. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +0 -6
  81. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts +16 -9
  82. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  83. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +4 -0
  84. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +13 -2
  85. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/schema.ts +1 -0
  86. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  87. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +50 -0
  88. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +60 -7
  89. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/table.ts +6 -4
  90. 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 DefaultTheme;
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.
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  title: Blogs
3
+ pageClass: cabloy-blogs-index
4
+ aside: false
3
5
  ---
4
6
 
5
7
  # Blogs
@@ -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. This makes the declaration a **placement overlay**, not an allow-list. 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).
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 omitted fields
224
+ ### Eligible and declared fields
225
225
 
226
- Only schema properties with `rest.visible !== false` are eligible. When an eligible visible field is absent from `formLayout`, the resolver appends it as a root-level field after the declared nodes, in schema-property order.
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, otherwise unplaced eligible canonical fields are still appended at the root.
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
- If a field must not render, make it invisible in schema metadata. Leaving it out of `formLayout.children` is not enough.
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. Unlisted visible fields are appended; unknown and duplicate declarations are silently pruned from the rendered plan.
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 are omitted visible fields appended or duplicate fields removed?
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, root append, and tab paths
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`. Duplicate tracking and tab-path bookkeeping use the canonical key, while visible canonical fields not placed by a surviving declaration are appended afterward. See [Form Layout Guide](/frontend/form-layout-guide#how-the-resolver-handles-the-declared-tree) for the full DTO authoring rules.
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 the controller bean is reactive and render has read its fields, changes such as:
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
- behave the way a Vue reader would expect at the reactive-engine level:
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
- So the runtime behavior is still recognizably Vue-like.
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 creates controller bean
264
- -> BeanContainer applies reactive(...)
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 controller fields
269
- -> field mutation invalidates dependencies
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: