cabloy 5.1.59 → 5.1.60
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/.claude/skills/cabloy-contract-loop/SKILL.md +16 -0
- package/.claude/skills/cabloy-contract-loop/references/contract-loop-map.md +26 -0
- package/.claude/skills/cabloy-contract-loop/references/resource-custom-state-pattern.md +144 -0
- package/.claude/skills/cabloy-contract-loop/references/verification-checklist.md +18 -0
- package/.claude/skills/cabloy-resource-field-update/SKILL.md +267 -0
- package/.claude/skills/cabloy-resource-field-update/evals/evals.json +53 -0
- package/.claude/skills/cabloy-resource-field-update/references/custom-renderer-demo-checklist.md +102 -0
- package/.claude/skills/cabloy-resource-field-update/references/field-update-decision-tree.md +120 -0
- package/.claude/skills/cabloy-resource-field-update/references/follow-up-checklist.md +80 -0
- package/.claude/skills/cabloy-resource-field-update/references/verification-checklist.md +97 -0
- package/.github/workflows/docs-pages.yml +2 -0
- package/.github/workflows/vona-cov-pg.yml +2 -0
- package/.github/workflows/vona-test-crud.yml +4 -2
- package/.github/workflows/vona-test-mysql.yml +2 -0
- package/.github/workflows/vona-test-pg.yml +2 -0
- package/.github/workflows/vona-test-sqlite3.yml +2 -0
- package/.github/workflows/vona-tsc.yml +2 -0
- package/.github/workflows/zova-ui.yml +2 -0
- package/.gitignore +0 -4
- package/CHANGELOG.md +30 -0
- package/CLAUDE.md +2 -0
- package/README.md +15 -0
- package/cabloy-docs/.vitepress/config.mjs +43 -0
- package/cabloy-docs/ai/class-placement-rule.md +2 -0
- package/cabloy-docs/ai/cli-to-skill-map.md +7 -0
- package/cabloy-docs/ai/future-skill-roadmap.md +17 -2
- package/cabloy-docs/backend/bean-scene-authoring.md +350 -0
- package/cabloy-docs/backend/cli.md +26 -1
- package/cabloy-docs/backend/foundation.md +28 -3
- package/cabloy-docs/backend/introduction.md +8 -0
- package/cabloy-docs/backend/service-guide.md +2 -0
- package/cabloy-docs/backend/websocket-call-flow.md +435 -0
- package/cabloy-docs/backend/websocket-guide.md +455 -0
- package/cabloy-docs/backend/websocket-protocol-guide.md +381 -0
- package/cabloy-docs/backend/websocket-usage-guide.md +356 -0
- package/cabloy-docs/frontend/bean-scene-authoring.md +372 -0
- package/cabloy-docs/frontend/behavior-guide.md +449 -0
- package/cabloy-docs/frontend/cli.md +12 -0
- package/cabloy-docs/frontend/introduction.md +5 -0
- package/cabloy-docs/frontend/ioc-and-beans.md +10 -9
- package/cabloy-docs/frontend/router-tabs-admin-web-comparison.md +206 -0
- package/cabloy-docs/frontend/router-tabs-introduction.md +106 -0
- package/cabloy-docs/frontend/router-tabs-mechanism.md +469 -0
- package/cabloy-docs/frontend/router-tabs-overview.md +227 -0
- package/cabloy-docs/frontend/router-tabs-route-meta-cookbook.md +343 -0
- package/cabloy-docs/frontend/ssr-architecture-overview.md +211 -0
- package/cabloy-docs/frontend/ssr-build-deploy-guide.md +308 -0
- package/cabloy-docs/frontend/ssr-review-checklist.md +184 -0
- package/cabloy-docs/frontend/ssr-troubleshooting-guide.md +301 -0
- package/cabloy-docs/fullstack/framework-performance.md +3 -3
- package/cabloy-docs/fullstack/introduction.md +29 -0
- package/cabloy-docs/fullstack/quickstart.md +7 -1
- package/cabloy-docs/fullstack/tutorial-1-first-module.md +111 -0
- package/cabloy-docs/fullstack/tutorial-2-first-crud.md +122 -0
- package/cabloy-docs/fullstack/tutorial-3-frontend-metadata-sharing.md +131 -0
- package/cabloy-docs/fullstack/tutorial-4-custom-level-renderers.md +119 -0
- package/cabloy-docs/fullstack/tutorial-5-backend-contract-sharing.md +144 -0
- package/cabloy-docs/fullstack/tutorial-6-one-contract-four-uses.md +168 -0
- package/cabloy-docs/fullstack/tutorials-overview.md +179 -0
- package/cabloy-docs/index.md +4 -3
- package/cabloy-docs/reference/bean-scene-boilerplates.md +73 -0
- package/cabloy-docs/reference/cli-reference.md +2 -0
- package/package.json +6 -2
- package/scripts/init.ts +18 -2
- package/vona/packages-cli/cabloy-cli/package.json +2 -2
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.create.module.ts +4 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +133 -1088
- package/vona/pnpm-workspace.yaml +0 -1
- package/vona/src/suite-vendor/a-vona/modules/a-core/assets/static/img/vona.svg +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/package.original.json +1 -1
- package/zova/packages-cli/cli/package.json +3 -3
- package/zova/packages-cli/cli-set-front/cli/templates/init/icon/boilerplate/icons/default/zova.svg +1 -1
- package/zova/packages-cli/cli-set-front/package.json +3 -3
- package/zova/packages-cli/cli-set-front/src/lib/bean/cli.create.module.ts +4 -0
- package/zova/packages-cli/cli-set-front/src/lib/command/create.bean.ts +5 -1
- package/zova/packages-utils/zova-vite/package.json +2 -2
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +284 -1313
- package/zova/pnpm-workspace.yaml +0 -1
- package/zova/src/suite/a-home/modules/home-icon/icons/social/cabloy.svg +1 -1
- package/zova/src/suite/a-home/modules/home-icon/icons/social/vona.svg +1 -1
- package/zova/src/suite/a-home/modules/home-icon/icons/social/zova.svg +1 -1
- package/zova/src/suite/a-home/modules/home-icon/src/.metadata/icons/groups/social.svg +3 -3
- package/zova/src/suite/cabloy-basic/modules/basic-select/src/component/formFieldSelect/controller.tsx +9 -0
- 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 +66 -16
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +60 -18
- package/zova/src/suite-vendor/a-zova/modules/a-table/cli/tableActionRow/boilerplate/{{sceneName}}.{{beanName}}.tsx_ +6 -1
- package/zova/src/suite-vendor/a-zova/modules/a-table/cli/tableCell/boilerplate/{{sceneName}}.{{beanName}}.tsx_ +6 -1
- package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +4 -4
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
# Router Tabs Mechanism
|
|
2
|
+
|
|
3
|
+
This guide explains how the router-tabs mechanism works in Zova within the Cabloy monorepo.
|
|
4
|
+
|
|
5
|
+
For the business meaning of the mechanism, see [Router Tabs Overview](/frontend/router-tabs-overview).
|
|
6
|
+
|
|
7
|
+
## Why this mechanism exists
|
|
8
|
+
|
|
9
|
+
The router-tabs mechanism is designed to support a workbench-style frontend navigation model.
|
|
10
|
+
|
|
11
|
+
Its main purpose is to separate:
|
|
12
|
+
|
|
13
|
+
- business-level grouping
|
|
14
|
+
- page-instance-level switching
|
|
15
|
+
|
|
16
|
+
In code, that separation is expressed mainly through:
|
|
17
|
+
|
|
18
|
+
- `tabKey`
|
|
19
|
+
- `componentKey`
|
|
20
|
+
|
|
21
|
+
These two fields are related, but they do not mean the same thing.
|
|
22
|
+
|
|
23
|
+
In the current Cabloy Basic frontend source, the shared router-tabs model lives in:
|
|
24
|
+
|
|
25
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts`
|
|
26
|
+
|
|
27
|
+
The Admin layout integration appears in:
|
|
28
|
+
|
|
29
|
+
- `zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx`
|
|
30
|
+
- `zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.tabs.tsx`
|
|
31
|
+
|
|
32
|
+
The Web layout reuses the same model through:
|
|
33
|
+
|
|
34
|
+
- `zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx`
|
|
35
|
+
- `zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.tabs.tsx`
|
|
36
|
+
|
|
37
|
+
## Core data model
|
|
38
|
+
|
|
39
|
+
The main tab model is `RouteTab`.
|
|
40
|
+
|
|
41
|
+
Representative shape:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
export interface RouteTab extends RouteTabBase {
|
|
45
|
+
items?: IRouteViewRouteItem[];
|
|
46
|
+
updatedAt: number;
|
|
47
|
+
info: RouteTabInfo;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A `RouteTab` represents one level-1 tab.
|
|
52
|
+
|
|
53
|
+
Its `items` array contains the level-2 tab items associated with that level-1 tab.
|
|
54
|
+
|
|
55
|
+
Representative related types include:
|
|
56
|
+
|
|
57
|
+
- `RouteTab`
|
|
58
|
+
- `RouteTabInfo`
|
|
59
|
+
- `IRouteViewRouteItem`
|
|
60
|
+
- `IRouteViewRouteMeta`
|
|
61
|
+
- `IPageMeta`
|
|
62
|
+
|
|
63
|
+
The important conceptual split is:
|
|
64
|
+
|
|
65
|
+
- `RouteTab` is the level-1 workspace
|
|
66
|
+
- `RouteTab.items[]` are the level-2 work items
|
|
67
|
+
|
|
68
|
+
Representative source definitions:
|
|
69
|
+
|
|
70
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/types/tabs.ts`
|
|
71
|
+
- `zova/src/suite-vendor/a-zova/modules/a-router/src/types/routerView.ts`
|
|
72
|
+
- `zova/src/suite-vendor/a-zova/modules/a-router/src/types/pageMeta.ts`
|
|
73
|
+
|
|
74
|
+
## Route-to-tab mapping
|
|
75
|
+
|
|
76
|
+
When a route is processed, the model prepares a route meta record containing:
|
|
77
|
+
|
|
78
|
+
- `fullPath`
|
|
79
|
+
- `componentKey`
|
|
80
|
+
- `tabKey`
|
|
81
|
+
- `keepAlive`
|
|
82
|
+
|
|
83
|
+
Representative pattern:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
prepareRouteMeta(route: RouteLocationNormalizedLoadedGeneric): IRouteViewRouteMeta {
|
|
87
|
+
const fullPath = route.fullPath;
|
|
88
|
+
const componentKey = this.__handleRoutePropComponentKey(route);
|
|
89
|
+
const tabKey = this._handleRouteProp(route, 'tabKey') || componentKey;
|
|
90
|
+
const keepAlive = this._handleRouteProp(route, 'keepAlive');
|
|
91
|
+
return { tabKey, componentKey, fullPath, keepAlive };
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This is the central mapping step from routing state into tab state.
|
|
96
|
+
|
|
97
|
+
Representative implementation:
|
|
98
|
+
|
|
99
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts` lines 493-530
|
|
100
|
+
|
|
101
|
+
## `tabKey`: level-1 grouping identity
|
|
102
|
+
|
|
103
|
+
`tabKey` decides which level-1 tab a route belongs to.
|
|
104
|
+
|
|
105
|
+
This is the grouping key.
|
|
106
|
+
|
|
107
|
+
If several routes should belong to the same business workspace, they should share the same `tabKey`.
|
|
108
|
+
|
|
109
|
+
If a route does not explicitly provide one, the mechanism falls back to `componentKey`.
|
|
110
|
+
|
|
111
|
+
That fallback means:
|
|
112
|
+
|
|
113
|
+
- every route can still open as a tab
|
|
114
|
+
- a route only joins an existing level-1 group when grouping is intentionally defined
|
|
115
|
+
|
|
116
|
+
This makes `tabKey` the business-grouping surface rather than the page-instance surface.
|
|
117
|
+
|
|
118
|
+
### Recommended `tabKey` rule
|
|
119
|
+
|
|
120
|
+
Use a stable `tabKey` for the durable business workspace identity, not for a temporary route variant.
|
|
121
|
+
|
|
122
|
+
Good candidates include:
|
|
123
|
+
|
|
124
|
+
- a module link such as `/user/list`
|
|
125
|
+
- a stable named business entry
|
|
126
|
+
- a menu identity that should continue to represent the same workspace over time
|
|
127
|
+
|
|
128
|
+
Avoid using `tabKey` values that change only because query, params, or transient task context changes.
|
|
129
|
+
|
|
130
|
+
## `componentKey`: level-2 instance identity
|
|
131
|
+
|
|
132
|
+
`componentKey` decides whether the current route should be treated as the same page instance or as a distinct one.
|
|
133
|
+
|
|
134
|
+
This is the level-2 identity key.
|
|
135
|
+
|
|
136
|
+
The mechanism first checks route meta for `componentKey`. If it is not provided, it derives the value from route name or route path.
|
|
137
|
+
|
|
138
|
+
Representative logic:
|
|
139
|
+
|
|
140
|
+
- explicit `meta.componentKey` has priority
|
|
141
|
+
- if route name exists and `componentKeyMode === 'nameOnly'`, use the route name
|
|
142
|
+
- otherwise use the route path
|
|
143
|
+
|
|
144
|
+
This matters because different pages need different reuse rules.
|
|
145
|
+
|
|
146
|
+
Representative implementation:
|
|
147
|
+
|
|
148
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts` lines 519-530
|
|
149
|
+
- `zova/src/suite/a-demo/modules/demo-basic/src/routes.ts` lines 20-28
|
|
150
|
+
|
|
151
|
+
### Example: shared instance
|
|
152
|
+
|
|
153
|
+
If a route uses `componentKeyMode: 'nameOnly'`, routes with different params can still share one logical page instance.
|
|
154
|
+
|
|
155
|
+
This is useful when the page should behave as one reusable workspace rather than opening a separate tab for each path variation.
|
|
156
|
+
|
|
157
|
+
### Example: separate instance
|
|
158
|
+
|
|
159
|
+
If the effective `componentKey` changes with the route path, different page entries can appear as separate level-2 tab items.
|
|
160
|
+
|
|
161
|
+
This is useful when different records or route shapes should remain independently open.
|
|
162
|
+
|
|
163
|
+
### Recommended `componentKey` rule
|
|
164
|
+
|
|
165
|
+
Use `componentKey` to answer one question only:
|
|
166
|
+
|
|
167
|
+
- should these route visits reuse one page instance, or should they remain separately open?
|
|
168
|
+
|
|
169
|
+
If the answer is reuse, keep `componentKey` coarse and stable.
|
|
170
|
+
|
|
171
|
+
If the answer is parallel open work, keep `componentKey` fine enough to distinguish those instances.
|
|
172
|
+
|
|
173
|
+
Do not use `componentKey` as a substitute for business grouping. That is `tabKey`'s job.
|
|
174
|
+
|
|
175
|
+
## Tab creation flow
|
|
176
|
+
|
|
177
|
+
The main flow is:
|
|
178
|
+
|
|
179
|
+
1. route enters the router-view tab system
|
|
180
|
+
2. route meta is prepared
|
|
181
|
+
3. the model calls `addTab`
|
|
182
|
+
4. the system either creates a new level-1 tab or updates an existing one
|
|
183
|
+
5. the current active tab keys are updated
|
|
184
|
+
|
|
185
|
+
Representative route-forwarding pattern:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
forwardRoute(route: RouteLocationNormalizedLoadedGeneric) {
|
|
189
|
+
const routeMeta = this.prepareRouteMeta(route);
|
|
190
|
+
this.addTab(routeMeta);
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Inside `addTab`, the mechanism:
|
|
195
|
+
|
|
196
|
+
- finds the existing level-1 tab by `tabKey`
|
|
197
|
+
- resolves the display info for that tab
|
|
198
|
+
- creates a new tab if needed
|
|
199
|
+
- inserts a newly created level-1 tab near the current context while keeping affixed tabs as a contiguous prefix
|
|
200
|
+
- otherwise updates the tab and its items without reordering an already existing level-1 tab
|
|
201
|
+
|
|
202
|
+
Representative implementation:
|
|
203
|
+
|
|
204
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts` lines 122-184
|
|
205
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts` lines 483-486
|
|
206
|
+
|
|
207
|
+
## Level-1 tab structure and the anchor item
|
|
208
|
+
|
|
209
|
+
When a new level-1 tab is created, the first item is usually created from the current route itself.
|
|
210
|
+
|
|
211
|
+
That first item often acts as the anchor route for the level-1 tab.
|
|
212
|
+
|
|
213
|
+
This is why the items list may contain an item whose `componentKey` equals `tabKey`.
|
|
214
|
+
|
|
215
|
+
In the Admin layout, level-2 rendering intentionally skips that first anchor item. That means:
|
|
216
|
+
|
|
217
|
+
- the level-1 tab still has a route anchor
|
|
218
|
+
- the level-2 row only shows the additional work items
|
|
219
|
+
|
|
220
|
+
This is an important part of the mechanism.
|
|
221
|
+
|
|
222
|
+
The shared model also preserves these ordering rules:
|
|
223
|
+
|
|
224
|
+
- the anchor item stays first within the workspace item list
|
|
225
|
+
- new level-2 items open near the current active item when they belong to the current workspace
|
|
226
|
+
- revisiting an already open level-2 item updates it in place instead of moving it
|
|
227
|
+
|
|
228
|
+
Representative implementation:
|
|
229
|
+
|
|
230
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts` lines 142-158
|
|
231
|
+
- `zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.tabs.tsx` lines 67-70
|
|
232
|
+
|
|
233
|
+
## Tab activation behavior
|
|
234
|
+
|
|
235
|
+
Activating a level-1 tab is different from activating a level-2 tab.
|
|
236
|
+
|
|
237
|
+
### Activating a level-1 tab
|
|
238
|
+
|
|
239
|
+
When a level-1 tab becomes active, the system tries to navigate back through that tab's anchor route or first route item.
|
|
240
|
+
|
|
241
|
+
Representative behavior:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
const tabItemFirst = tab.items?.[0];
|
|
245
|
+
const path = tabItemFirst?.componentKey === tabKey ? tabItemFirst.fullPath : tabKey;
|
|
246
|
+
await this.$router.push(path);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
This means a level-1 tab is not just a label. It remains connected to a restorable route context.
|
|
250
|
+
|
|
251
|
+
### Activating a level-2 tab
|
|
252
|
+
|
|
253
|
+
When a level-2 tab item becomes active, the system pushes its own `fullPath`.
|
|
254
|
+
|
|
255
|
+
That gives each work item an exact route target.
|
|
256
|
+
|
|
257
|
+
Representative implementation:
|
|
258
|
+
|
|
259
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts` lines 351-373
|
|
260
|
+
|
|
261
|
+
## Tab info and menu integration
|
|
262
|
+
|
|
263
|
+
In the Admin layout, level-1 tab info is usually derived from the menu model.
|
|
264
|
+
|
|
265
|
+
Representative initialization pattern:
|
|
266
|
+
|
|
267
|
+
- `getInitialTabs`
|
|
268
|
+
- `getTabInfo`
|
|
269
|
+
|
|
270
|
+
This means the level-1 title and icon are normally tied to business navigation identity, not to the runtime page title of a specific inner task.
|
|
271
|
+
|
|
272
|
+
That is why the first-level and second-level labels come from different sources.
|
|
273
|
+
|
|
274
|
+
Representative implementation:
|
|
275
|
+
|
|
276
|
+
- `zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx` lines 75-107
|
|
277
|
+
- `zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx` lines 73-107
|
|
278
|
+
|
|
279
|
+
## Page metadata and level-2 state
|
|
280
|
+
|
|
281
|
+
Level-2 tab items can carry `pageMeta`.
|
|
282
|
+
|
|
283
|
+
Representative page metadata includes:
|
|
284
|
+
|
|
285
|
+
- `pageTitle`
|
|
286
|
+
- `pageDirty`
|
|
287
|
+
- `formMeta`
|
|
288
|
+
|
|
289
|
+
This metadata shapes the visible state of the level-2 tab.
|
|
290
|
+
|
|
291
|
+
### `pageTitle`
|
|
292
|
+
|
|
293
|
+
`pageTitle` is typically used as the displayed title of the level-2 item.
|
|
294
|
+
|
|
295
|
+
This is what allows the second level to show task-specific names instead of module names.
|
|
296
|
+
|
|
297
|
+
### `pageDirty`
|
|
298
|
+
|
|
299
|
+
If `pageDirty` is true, the UI can show a dirty indicator such as an asterisk.
|
|
300
|
+
|
|
301
|
+
This tells the user that the current work item has unsaved changes.
|
|
302
|
+
|
|
303
|
+
### `formMeta.formScene`
|
|
304
|
+
|
|
305
|
+
The form scene can also affect the icon or visual state.
|
|
306
|
+
|
|
307
|
+
Typical cases include:
|
|
308
|
+
|
|
309
|
+
- `create`
|
|
310
|
+
- `edit`
|
|
311
|
+
|
|
312
|
+
This helps the user distinguish draft-like work from modification work at a glance.
|
|
313
|
+
|
|
314
|
+
Representative implementation:
|
|
315
|
+
|
|
316
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts` lines 212-233
|
|
317
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts` lines 488-490
|
|
318
|
+
- `zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.tabs.tsx` lines 113-128
|
|
319
|
+
|
|
320
|
+
## Keep-alive behavior
|
|
321
|
+
|
|
322
|
+
The model computes a `keepAliveInclude` list from tab items.
|
|
323
|
+
|
|
324
|
+
Representative rule:
|
|
325
|
+
|
|
326
|
+
- include each `componentKey` whose tab item is not explicitly `keepAlive: false`
|
|
327
|
+
- avoid duplicates
|
|
328
|
+
|
|
329
|
+
This keeps routed page instances alive while they remain part of the active tab model.
|
|
330
|
+
|
|
331
|
+
This is a key part of the workbench experience because users expect tabs to preserve state across switching.
|
|
332
|
+
|
|
333
|
+
For the broader SSR client-only boundary pattern, also see [SSR ClientOnly](/frontend/ssr-client-only).
|
|
334
|
+
|
|
335
|
+
Representative implementation:
|
|
336
|
+
|
|
337
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts` lines 452-465
|
|
338
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/component/routerViewTabs/controller.tsx` lines 36-42
|
|
339
|
+
|
|
340
|
+
## Cache behavior
|
|
341
|
+
|
|
342
|
+
The tab model supports both in-memory and persistent state depending on configuration.
|
|
343
|
+
|
|
344
|
+
Representative options include:
|
|
345
|
+
|
|
346
|
+
- `cache`
|
|
347
|
+
- `max`
|
|
348
|
+
- `maxItems`
|
|
349
|
+
|
|
350
|
+
If `cache` is enabled, the model can store tab state through a persistent model-state path instead of memory-only state.
|
|
351
|
+
|
|
352
|
+
This allows the workbench state to survive refreshes more effectively.
|
|
353
|
+
|
|
354
|
+
### Why dirty state is reset
|
|
355
|
+
|
|
356
|
+
When cached tab state is restored, the mechanism resets `pageDirty` flags.
|
|
357
|
+
|
|
358
|
+
This is an important safety boundary.
|
|
359
|
+
|
|
360
|
+
A restored tab should not automatically claim that the user still has unsaved work unless the application can truly re-establish that state reliably.
|
|
361
|
+
|
|
362
|
+
That is why the mechanism restores the workspace structure but clears fragile dirty markers.
|
|
363
|
+
|
|
364
|
+
Representative implementation:
|
|
365
|
+
|
|
366
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts` lines 71-87
|
|
367
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts` lines 532-545
|
|
368
|
+
- `zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.tabs.tsx` lines 24-25
|
|
369
|
+
- `zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.tabs.tsx` lines 25-26
|
|
370
|
+
|
|
371
|
+
## Capacity control
|
|
372
|
+
|
|
373
|
+
The model supports two capacity limits:
|
|
374
|
+
|
|
375
|
+
- `max`: maximum number of level-1 tabs
|
|
376
|
+
- `maxItems`: maximum number of level-2 items per level-1 tab
|
|
377
|
+
|
|
378
|
+
When limits are exceeded, the mechanism prunes older entries based on update time.
|
|
379
|
+
|
|
380
|
+
This keeps the workbench from growing without bound.
|
|
381
|
+
|
|
382
|
+
There is also special handling for affixed tabs such as fixed entry tabs. Those should not be pruned in the same way as normal tabs.
|
|
383
|
+
|
|
384
|
+
Representative implementation:
|
|
385
|
+
|
|
386
|
+
- `zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts` lines 382-423
|
|
387
|
+
|
|
388
|
+
## Rendering model in Admin and Web layouts
|
|
389
|
+
|
|
390
|
+
The current Cabloy Basic source shows two different presentations of the same model.
|
|
391
|
+
|
|
392
|
+
### Admin layout
|
|
393
|
+
|
|
394
|
+
The Admin layout renders:
|
|
395
|
+
|
|
396
|
+
- level-1 tabs in a lifted tab row
|
|
397
|
+
- level-2 tabs in a bordered tab row for the current level-1 tab
|
|
398
|
+
|
|
399
|
+
Important implementation points include:
|
|
400
|
+
|
|
401
|
+
- level-1 uses tab info such as title and icon
|
|
402
|
+
- level-2 uses page-level metadata such as `pageTitle`
|
|
403
|
+
- level-2 skips the anchor item
|
|
404
|
+
- close actions differ for level-1 and level-2 items
|
|
405
|
+
|
|
406
|
+
Representative implementation:
|
|
407
|
+
|
|
408
|
+
- `zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.tabs.tsx`
|
|
409
|
+
|
|
410
|
+
### Web layout
|
|
411
|
+
|
|
412
|
+
The Web layout reuses the same router-tabs model but renders top-level workspaces as horizontal menu items and nested menu groups.
|
|
413
|
+
|
|
414
|
+
That means the framework contract should be understood as a route-grouping and workbench-state mechanism, not only as one specific double-tab-row UI.
|
|
415
|
+
|
|
416
|
+
Representative implementation:
|
|
417
|
+
|
|
418
|
+
- `zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.tabs.tsx`
|
|
419
|
+
|
|
420
|
+
## Extension guidance
|
|
421
|
+
|
|
422
|
+
When designing pages that participate in this mechanism, ask these questions:
|
|
423
|
+
|
|
424
|
+
### Should several routes belong to one business workspace?
|
|
425
|
+
|
|
426
|
+
If yes, define a stable `tabKey`.
|
|
427
|
+
|
|
428
|
+
### Should several route variants reuse one page instance?
|
|
429
|
+
|
|
430
|
+
If yes, consider `componentKey` or `componentKeyMode`.
|
|
431
|
+
|
|
432
|
+
### Should the page show task-specific titles or state in the second-level tabs?
|
|
433
|
+
|
|
434
|
+
If yes, provide `pageMeta` updates such as `pageTitle`, `pageDirty`, or form scene metadata.
|
|
435
|
+
|
|
436
|
+
### Should the workbench survive refreshes?
|
|
437
|
+
|
|
438
|
+
If yes, evaluate whether `cache` should be enabled.
|
|
439
|
+
|
|
440
|
+
### Should users be able to open several records in parallel?
|
|
441
|
+
|
|
442
|
+
If yes, ensure that `componentKey` is fine-grained enough to keep those records distinct.
|
|
443
|
+
|
|
444
|
+
## Common pitfalls
|
|
445
|
+
|
|
446
|
+
Typical mistakes include:
|
|
447
|
+
|
|
448
|
+
- using `tabKey` as if it were only a route identity
|
|
449
|
+
- making `tabKey` unstable across equivalent business routes
|
|
450
|
+
- making `componentKey` too coarse and collapsing distinct tasks into one tab item
|
|
451
|
+
- making `componentKey` too fine and opening too many nearly identical tab items
|
|
452
|
+
- assuming dirty state can be restored safely after every reload
|
|
453
|
+
- treating one layout's rendering choice as if it were the full framework contract
|
|
454
|
+
|
|
455
|
+
## Summary
|
|
456
|
+
|
|
457
|
+
The router-tabs mechanism depends on a clear split:
|
|
458
|
+
|
|
459
|
+
- `tabKey` groups routes into level-1 workspaces
|
|
460
|
+
- `componentKey` identifies level-2 work items or page instances
|
|
461
|
+
|
|
462
|
+
With page metadata, keep-alive behavior, caching, and capacity control layered on top, the result is a workbench-style navigation model that fits complex admin workflows much better than plain route switching.
|
|
463
|
+
|
|
464
|
+
## See also
|
|
465
|
+
|
|
466
|
+
- [Router Tabs Introduction](/frontend/router-tabs-introduction)
|
|
467
|
+
- [Router Tabs Overview](/frontend/router-tabs-overview)
|
|
468
|
+
- [Router Tabs Route Meta Cookbook](/frontend/router-tabs-route-meta-cookbook)
|
|
469
|
+
- [Router Tabs Admin and Web Comparison](/frontend/router-tabs-admin-web-comparison)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Router Tabs Overview
|
|
2
|
+
|
|
3
|
+
This guide explains the business meaning of the router tabs mechanism in Zova within the Cabloy monorepo.
|
|
4
|
+
|
|
5
|
+
In the current Cabloy Basic frontend source, the same mechanism supports more than one layout style. The Admin layout shows the two-level meaning explicitly, while the Web layout reuses the same model with a menu-oriented top-level presentation.
|
|
6
|
+
|
|
7
|
+
## Why router tabs matter
|
|
8
|
+
|
|
9
|
+
In a simple application, a left-side menu plus route navigation can be enough.
|
|
10
|
+
|
|
11
|
+
In an admin-style application, that is often not enough.
|
|
12
|
+
|
|
13
|
+
Users usually need to:
|
|
14
|
+
|
|
15
|
+
- move across several business modules
|
|
16
|
+
- keep the current working context of each module
|
|
17
|
+
- open more than one task inside the same module
|
|
18
|
+
- switch between unfinished tasks without losing their place
|
|
19
|
+
|
|
20
|
+
That is the main problem solved by router tabs.
|
|
21
|
+
|
|
22
|
+
The mechanism is not only a visual tab bar. It is a workbench-style navigation model.
|
|
23
|
+
|
|
24
|
+
## The two levels at a glance
|
|
25
|
+
|
|
26
|
+
In the Admin layout, the two levels serve different purposes:
|
|
27
|
+
|
|
28
|
+
- level-1 tabs represent business modules or workspaces
|
|
29
|
+
- level-2 tabs represent concrete tasks or page instances inside the current workspace
|
|
30
|
+
|
|
31
|
+
This separation is the key idea.
|
|
32
|
+
|
|
33
|
+
A user should be able to say both:
|
|
34
|
+
|
|
35
|
+
- "I am working in Order Management"
|
|
36
|
+
- "Inside Order Management, I currently have Create Order and Edit Order 2026001 open"
|
|
37
|
+
|
|
38
|
+
That is exactly what the two levels express.
|
|
39
|
+
|
|
40
|
+
## Business meaning of level-1 tabs
|
|
41
|
+
|
|
42
|
+
A level-1 tab represents the broader business area the user is working in.
|
|
43
|
+
|
|
44
|
+
Typical examples include:
|
|
45
|
+
|
|
46
|
+
- Home
|
|
47
|
+
- User Management
|
|
48
|
+
- Order Management
|
|
49
|
+
- Content Management
|
|
50
|
+
|
|
51
|
+
This level should stay relatively stable.
|
|
52
|
+
|
|
53
|
+
Its job is not to reflect every temporary page the user opens. Its job is to preserve the higher-level workspace identity.
|
|
54
|
+
|
|
55
|
+
In practice, this gives the user a stable top-level navigation surface even when the detailed work inside a module changes frequently.
|
|
56
|
+
|
|
57
|
+
In Cabloy Basic Admin, the current level-1 title and icon are typically derived from the menu model rather than from the current inner page title. That is why the first level behaves more like a business workspace than like a raw route list.
|
|
58
|
+
|
|
59
|
+
## Business meaning of level-2 tabs
|
|
60
|
+
|
|
61
|
+
A level-2 tab represents a concrete working item inside the current level-1 tab.
|
|
62
|
+
|
|
63
|
+
Typical examples include:
|
|
64
|
+
|
|
65
|
+
- a create page
|
|
66
|
+
- an edit page
|
|
67
|
+
- a detail page
|
|
68
|
+
- a preview page
|
|
69
|
+
- multiple records opened in parallel
|
|
70
|
+
|
|
71
|
+
This makes the interaction model much closer to a desktop workbench or IDE.
|
|
72
|
+
|
|
73
|
+
Instead of repeatedly returning to a list and reopening records one by one, the user can keep several work items open and switch between them directly.
|
|
74
|
+
|
|
75
|
+
In practice, the second level is where task identity becomes visible. A level-2 item can represent "Create User", "Edit User A", or "Edit User B" even though all of them still belong to the same level-1 workspace such as User Management.
|
|
76
|
+
|
|
77
|
+
## Why not use only one level
|
|
78
|
+
|
|
79
|
+
A single-level tab system often becomes unstable in admin workflows.
|
|
80
|
+
|
|
81
|
+
If every open page appears at the top level, the tab bar can quickly become noisy:
|
|
82
|
+
|
|
83
|
+
- User Management
|
|
84
|
+
- Edit User A
|
|
85
|
+
- Edit User B
|
|
86
|
+
- Order Management
|
|
87
|
+
- Edit Order X
|
|
88
|
+
- Create Order
|
|
89
|
+
- Content Management
|
|
90
|
+
- Edit Article 1
|
|
91
|
+
|
|
92
|
+
At that point, the top level is no longer a clean business navigation layer.
|
|
93
|
+
|
|
94
|
+
The two-level design avoids that problem:
|
|
95
|
+
|
|
96
|
+
- level-1 tabs keep the business structure stable
|
|
97
|
+
- level-2 tabs hold the temporary work items
|
|
98
|
+
|
|
99
|
+
This keeps navigation and working context separate.
|
|
100
|
+
|
|
101
|
+
## Typical admin scenarios
|
|
102
|
+
|
|
103
|
+
Two-level router tabs are especially useful in these cases.
|
|
104
|
+
|
|
105
|
+
### Parallel work inside one module
|
|
106
|
+
|
|
107
|
+
A user may need to:
|
|
108
|
+
|
|
109
|
+
- create a new user
|
|
110
|
+
- edit an existing user
|
|
111
|
+
- compare two records
|
|
112
|
+
- check a detail page while keeping a form open
|
|
113
|
+
|
|
114
|
+
These are not separate business modules. They are parallel tasks inside the same module.
|
|
115
|
+
|
|
116
|
+
### Frequent switching across modules
|
|
117
|
+
|
|
118
|
+
A user may need to:
|
|
119
|
+
|
|
120
|
+
- inspect an order
|
|
121
|
+
- jump to a user profile
|
|
122
|
+
- return to inventory
|
|
123
|
+
- go back to the previous order task
|
|
124
|
+
|
|
125
|
+
The mechanism allows the user to switch business areas without losing the active work context of each area.
|
|
126
|
+
|
|
127
|
+
### Long-running form workflows
|
|
128
|
+
|
|
129
|
+
Some forms are not completed in one uninterrupted pass.
|
|
130
|
+
|
|
131
|
+
Users may leave a form, handle another task, and then return later.
|
|
132
|
+
|
|
133
|
+
A tabbed workbench model supports that better than a strict list-detail-return cycle.
|
|
134
|
+
|
|
135
|
+
## Visual and interaction meaning
|
|
136
|
+
|
|
137
|
+
The two levels carry different kinds of information.
|
|
138
|
+
|
|
139
|
+
### Level-1 tabs
|
|
140
|
+
|
|
141
|
+
Level-1 tabs usually communicate:
|
|
142
|
+
|
|
143
|
+
- business identity
|
|
144
|
+
- module title
|
|
145
|
+
- module icon
|
|
146
|
+
- whether the workspace is pinned or fixed
|
|
147
|
+
|
|
148
|
+
### Level-2 tabs
|
|
149
|
+
|
|
150
|
+
Level-2 tabs usually communicate:
|
|
151
|
+
|
|
152
|
+
- page or record title
|
|
153
|
+
- current task identity
|
|
154
|
+
- whether the page is dirty
|
|
155
|
+
- whether the page is in create or edit mode
|
|
156
|
+
|
|
157
|
+
This means the user can see not only where they are working, but also what state the current work item is in.
|
|
158
|
+
|
|
159
|
+
## Workbench continuity
|
|
160
|
+
|
|
161
|
+
Another important value is continuity.
|
|
162
|
+
|
|
163
|
+
A module tab is not just a shortcut to a menu entry. It acts more like a workspace anchor.
|
|
164
|
+
|
|
165
|
+
When the user returns to a level-1 tab, the system can restore the working context associated with that tab instead of forcing the user to start navigation from scratch each time.
|
|
166
|
+
|
|
167
|
+
This is one of the main reasons the mechanism feels more like a workbench than a simple menu wrapper.
|
|
168
|
+
|
|
169
|
+
## Admin and Web layout perspective
|
|
170
|
+
|
|
171
|
+
The current Cabloy Basic source shows that router tabs are a mechanism first and a specific visual shape second.
|
|
172
|
+
|
|
173
|
+
### Admin layout
|
|
174
|
+
|
|
175
|
+
The Admin layout exposes the two-level meaning directly:
|
|
176
|
+
|
|
177
|
+
- a first row for workspace-level tabs
|
|
178
|
+
- a second row for task-level items inside the active workspace
|
|
179
|
+
|
|
180
|
+
This is the clearest illustration of the level-1 and level-2 split.
|
|
181
|
+
|
|
182
|
+
### Web layout
|
|
183
|
+
|
|
184
|
+
The Web layout reuses the same underlying tab model but presents the first level more like a menu-driven horizontal workspace surface.
|
|
185
|
+
|
|
186
|
+
That means future contributors should avoid assuming that the framework contract is identical to one concrete tab-row UI.
|
|
187
|
+
|
|
188
|
+
The durable idea is the route-to-workspace model, not only the exact Admin visual treatment.
|
|
189
|
+
|
|
190
|
+
## When this pattern is a good fit
|
|
191
|
+
|
|
192
|
+
This pattern is a good fit when the frontend behaves like a business workbench, especially when users need:
|
|
193
|
+
|
|
194
|
+
- multi-step task flows
|
|
195
|
+
- repeated switching across modules
|
|
196
|
+
- several open records at once
|
|
197
|
+
- persistent working context
|
|
198
|
+
- form-heavy operations
|
|
199
|
+
|
|
200
|
+
It is less important for simple content sites or low-interaction flows where a single route transition is enough.
|
|
201
|
+
|
|
202
|
+
## Recommended design questions
|
|
203
|
+
|
|
204
|
+
When deciding whether a page or feature should participate in router tabs, ask these questions first:
|
|
205
|
+
|
|
206
|
+
1. is this a real business workspace or only a temporary route?
|
|
207
|
+
2. should several related pages stay grouped under one stable module identity?
|
|
208
|
+
3. do users need to keep more than one work item open at the same time?
|
|
209
|
+
4. does the page need visible task state such as dirty/create/edit indicators?
|
|
210
|
+
|
|
211
|
+
If most answers are yes, the router-tabs workbench model is usually a strong fit.
|
|
212
|
+
|
|
213
|
+
## Summary
|
|
214
|
+
|
|
215
|
+
The router tabs mechanism separates two different concerns:
|
|
216
|
+
|
|
217
|
+
- business-module switching
|
|
218
|
+
- task-level page switching inside the current module
|
|
219
|
+
|
|
220
|
+
That separation improves clarity, preserves working context, and supports parallel task handling in admin-style applications.
|
|
221
|
+
|
|
222
|
+
## See also
|
|
223
|
+
|
|
224
|
+
- [Router Tabs Introduction](/frontend/router-tabs-introduction)
|
|
225
|
+
- [Router Tabs Mechanism](/frontend/router-tabs-mechanism)
|
|
226
|
+
- [Router Tabs Route Meta Cookbook](/frontend/router-tabs-route-meta-cookbook)
|
|
227
|
+
- [Router Tabs Admin and Web Comparison](/frontend/router-tabs-admin-web-comparison)
|