cabloy 5.1.227 → 5.1.229
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/CHANGELOG.md +27 -0
- package/CONTRIBUTING.md +1 -1
- package/package.json +2 -2
- package/repo-agent-governance/managed-assets.json +30 -30
- package/repo-agent-governance/policies/repository.md +1 -1
- package/repo-agent-governance/skills/cabloy-backend-scaffold/SKILL.md +3 -3
- package/repo-agent-governance/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +2 -2
- package/repo-agent-governance/skills/cabloy-frontend-scaffold/SKILL.md +1 -0
- package/repo-agent-governance/skills/cabloy-master-detail/SKILL.md +2 -1
- package/repo-agent-governance/skills/cabloy-resource-field-update/SKILL.md +2 -0
- package/repo-agent-governance/skills/cabloy-resource-field-update/references/follow-up-checklist.md +11 -5
- package/repo-agent-governance/skills/cabloy-resource-field-update/references/verification-checklist.md +10 -14
- package/repo-agent-governance/skills/cabloy-spec-execution/evals/evals.json +4 -2
- package/repo-agent-governance/skills/cabloy-spec-execution/references/execution-protocol.md +1 -1
- package/repo-agent-governance/tests/evidence-paths.test.mjs +52 -17
- package/repo-agent-governance/tools/evidence-paths/sanitize.mjs +13 -13
- package/repo-docs/backend/migration-and-changes.md +21 -14
- package/repo-docs/backend/quickstart.md +1 -1
- package/repo-docs/backend/resource-field-update.md +3 -2
- package/repo-docs/backend/unit-testing.md +9 -3
- package/repo-docs/frontend/table-cell-cookbook.md +32 -0
- package/repo-docs/fullstack/quickstart.md +1 -1
- package/repo-docs/pnpm-lock.yaml +6 -6
- package/repo-docs/reference/repo-scripts.md +2 -2
- package/repo-e2e/specs/a-commerce.spec.ts +83 -9
- package/scripts/init.ts +14 -0
- package/vona/pnpm-lock.yaml +25 -82
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +2 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +2 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderShip.tsx +4 -2
- package/zova/package.original.json +1 -1
- package/zova/packages-cli/cli/package.json +5 -2
- package/zova/packages-cli/cli-set-front/package.json +5 -2
- package/zova/packages-cli/cli-set-front/src/lib/bean/cli.bin.buildRest.ts +5 -1
- package/zova/pnpm-lock.yaml +434 -276
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +23 -23
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +28 -12
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +245 -85
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionAdjustStock/controller.tsx +3 -2
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionShip/controller.tsx +82 -34
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +1 -0
|
@@ -69,7 +69,7 @@ function pathEnd(text, start, quoted) {
|
|
|
69
69
|
let end = start;
|
|
70
70
|
while (end < text.length) {
|
|
71
71
|
const char = text[end];
|
|
72
|
-
if (/[\
|
|
72
|
+
if (/[\p{Cc}"'`<>(){}[\]|,;!?]/u.test(char) || (!quoted && /\s/u.test(char))) break;
|
|
73
73
|
// Preserve stack line/column suffixes (and keep other colon-delimited prose out).
|
|
74
74
|
if (char === ':') break;
|
|
75
75
|
end++;
|
|
@@ -119,14 +119,15 @@ export function createMatcher({ repoRoot = DEFAULT_REPO_ROOT, maps = [] } = {})
|
|
|
119
119
|
);
|
|
120
120
|
|
|
121
121
|
const sep = '(?:\\\\{1,2}|/)';
|
|
122
|
-
const account =
|
|
122
|
+
const account =
|
|
123
|
+
'[^/\\\\\\s\\p{Cc}"\'`<>(){}[\\]|,;:!?]+(?: [^/\\\\\\s\\p{Cc}"\'`<>(){}[\\]|,;:!?]+)*';
|
|
123
124
|
const known = [
|
|
124
|
-
new RegExp(`/(?:Users|home)/${account}`, '
|
|
125
|
+
new RegExp(`/(?:Users|home)/${account}`, 'uy'),
|
|
125
126
|
/\/(?:private\/)?tmp/y,
|
|
126
127
|
/\/(?:private\/)?var\/folders/y,
|
|
127
|
-
new RegExp(`[A-Z]:${sep}(?:Users${sep}${account}|(?:Windows${sep})?Temp)`, '
|
|
128
|
+
new RegExp(`[A-Z]:${sep}(?:Users${sep}${account}|(?:Windows${sep})?Temp)`, 'iuy'),
|
|
128
129
|
// UNC is a filesystem convention; forward-slash protocol-relative URLs are not.
|
|
129
|
-
/\\{2,4}[^\\/\s"'`<>(){}[\]|,;:!?]+\\{1,2}[^\\/\s"'`<>(){}[\]|,;:!?]+/
|
|
130
|
+
/\\{2,4}[^\\/\s\p{Cc}"'`<>(){}[\]|,;:!?]+\\{1,2}[^\\/\s\p{Cc}"'`<>(){}[\]|,;:!?]+/uy,
|
|
130
131
|
];
|
|
131
132
|
|
|
132
133
|
function recognizeAt(text, start, quoted = false) {
|
|
@@ -135,7 +136,7 @@ export function createMatcher({ repoRoot = DEFAULT_REPO_ROOT, maps = [] } = {})
|
|
|
135
136
|
const match = entry.regex.exec(text);
|
|
136
137
|
if (!match) continue;
|
|
137
138
|
const after = start + match[0].length;
|
|
138
|
-
if (after < text.length && !/[/\\\s"'`<>(){}[\]|,;:!?]/u.test(text[after])) continue;
|
|
139
|
+
if (after < text.length && !/[/\\\s\p{Cc}"'`<>(){}[\]|,;:!?]/u.test(text[after])) continue;
|
|
139
140
|
const end = pathEnd(text, after, quoted);
|
|
140
141
|
const tail = normalizeTail(text.slice(after, end), entry.windows).replace(/^\/+|\/+$/gu, '');
|
|
141
142
|
const replacement =
|
|
@@ -153,7 +154,7 @@ export function createMatcher({ repoRoot = DEFAULT_REPO_ROOT, maps = [] } = {})
|
|
|
153
154
|
const match = regex.exec(text);
|
|
154
155
|
if (!match) continue;
|
|
155
156
|
const after = start + match[0].length;
|
|
156
|
-
if (after < text.length && !/[/\\\s"'`<>(){}[\]|,;:!?]/u.test(text[after])) continue;
|
|
157
|
+
if (after < text.length && !/[/\\\s\p{Cc}"'`<>(){}[\]|,;:!?]/u.test(text[after])) continue;
|
|
157
158
|
return { end: pathEnd(text, after, quoted), replacement: '<PRIVATE_PATH>' };
|
|
158
159
|
}
|
|
159
160
|
return null;
|
|
@@ -163,7 +164,7 @@ export function createMatcher({ repoRoot = DEFAULT_REPO_ROOT, maps = [] } = {})
|
|
|
163
164
|
if (typeof text !== 'string') fail('INVALID_TEXT');
|
|
164
165
|
const matches = [];
|
|
165
166
|
// Skip non-file URLs as units, including their paths, query strings and fragments.
|
|
166
|
-
const urls = /[A-Za-z][A-Za-z0-9+.-]{0,31}:\/\/[^\s"'`<>(){}[\]|]+/gu;
|
|
167
|
+
const urls = /[A-Za-z][A-Za-z0-9+.-]{0,31}:\/\/[^\s\p{Cc}"'`<>(){}[\]|]+/gu;
|
|
167
168
|
const protectedUrls = [...text.matchAll(urls)];
|
|
168
169
|
let urlIndex = 0;
|
|
169
170
|
for (let index = 0; index < text.length; index++) {
|
|
@@ -262,7 +263,6 @@ export async function sanitizeStream(input, output, { matcher = createMatcher()
|
|
|
262
263
|
parts.push(bytes.subarray(start, end));
|
|
263
264
|
if (newline < 0) break;
|
|
264
265
|
const line = Buffer.concat(parts, length);
|
|
265
|
-
if (binary(line)) fail('BINARY');
|
|
266
266
|
const text = decode(line);
|
|
267
267
|
await write(output, `${matcher.sanitize(text)}\n`);
|
|
268
268
|
parts = [];
|
|
@@ -272,7 +272,6 @@ export async function sanitizeStream(input, output, { matcher = createMatcher()
|
|
|
272
272
|
}
|
|
273
273
|
if (length) {
|
|
274
274
|
const line = Buffer.concat(parts, length);
|
|
275
|
-
if (binary(line)) fail('BINARY');
|
|
276
275
|
await write(output, matcher.sanitize(decode(line)));
|
|
277
276
|
}
|
|
278
277
|
if (outputFailed) fail('OUTPUT_ERROR');
|
|
@@ -464,10 +463,11 @@ Stack :line:column suffixes are preserved. Non-file URLs, unrelated API/containe
|
|
|
464
463
|
paths and unknown absolute roots are left unchanged: this is NOT a universal secret
|
|
465
464
|
scanner. Use explicit maps for other local roots; inspect output before publication.
|
|
466
465
|
UTF-8 decoding is fatal, with a 1 MiB byte limit per line (including CR, excluding LF).
|
|
467
|
-
|
|
466
|
+
sanitize preserves control characters without interpreting or stripping them; they do
|
|
467
|
+
not cause BINARY rejection. LF separates lines; CR counts toward the line byte limit.
|
|
468
468
|
Only complete validated lines are emitted; earlier sanitized lines may survive failure.
|
|
469
|
-
check limits each file to 8 MiB
|
|
470
|
-
scope-N/relative-filename:line CODE, never source lines or absolute scan roots.
|
|
469
|
+
check limits each file to 8 MiB and treats NUL, DEL and non-text C0 controls as binary.
|
|
470
|
+
It reports only scope-N/relative-filename:line CODE, never source lines or absolute scan roots.
|
|
471
471
|
Symlinks (including scope ancestors), special files, .env* files/directories, unreadable,
|
|
472
472
|
binary, invalid UTF-8 and over-limit files make checking incomplete. Scope trees must
|
|
473
473
|
remain stable while scanning; this is not an adversarial filesystem snapshot.
|
|
@@ -71,7 +71,9 @@ When the change belongs in the current version path rather than new migration hi
|
|
|
71
71
|
2. fold the schema change into the current version path;
|
|
72
72
|
3. do not create a new migration branch.
|
|
73
73
|
|
|
74
|
-
Ask before choosing this strategy; do not infer migration history from the field change alone.
|
|
74
|
+
Ask before choosing this strategy; do not infer migration history from the field change alone. Normal startup does not replay an already-applied positive-numbered migration branch. Editing that branch affects fresh installations and isolated test preparation, not a development database that has already applied it. A no-bump decision is not authorization to reset development data; if existing data must be preserved, agree on a new incremental migration instead.
|
|
75
|
+
|
|
76
|
+
Verify changes to migration, initialization, or seed behavior through managed isolated tests covering the affected behavior, preferably focused. A `meta.version.ts` edit alone does not require the full suite. See [Local development workflow](#local-development-workflow) for the separate verification and development-database decisions.
|
|
75
77
|
|
|
76
78
|
## `meta.version`
|
|
77
79
|
|
|
@@ -186,29 +188,33 @@ A practical sequence is:
|
|
|
186
188
|
1. decide whether to increment `fileVersion`
|
|
187
189
|
2. add or adjust entity/model structure
|
|
188
190
|
3. update the current or new `meta.version` path to match that decision
|
|
189
|
-
4.
|
|
190
|
-
5.
|
|
191
|
+
4. verify migration and resource behavior through managed isolated tests
|
|
192
|
+
5. separately decide whether and how to update the development database
|
|
191
193
|
|
|
192
194
|
This keeps schema change, backend contract change, and verification tightly connected.
|
|
193
195
|
|
|
194
196
|
## Local development workflow
|
|
195
197
|
|
|
196
|
-
|
|
198
|
+
Keep two questions separate: **how to verify the change** and **how to update development data**. Isolated tests do not update development databases, and a development reset is not a substitute for tests.
|
|
197
199
|
|
|
198
|
-
|
|
200
|
+
| Need | Path | Boundary |
|
|
201
|
+
| -------------------------------------------------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
202
|
+
| Verify migration, initialization, seed, and affected resource behavior | Managed isolated tests, preferably focused | Even a focused run prepares a fresh test database; it does not change dev data. |
|
|
203
|
+
| Apply a new module or pending versioned migration to an existing prepared dev database | Normal application startup | Applies pending versions without resetting existing data; ordinary dev startup does not repeat `seed()`. |
|
|
204
|
+
| Replay an edited, already-applied positive-numbered branch on disposable dev data | Separately approved `npm run db:reset` | Destructive recreation; a no-bump decision does not authorize reset. |
|
|
205
|
+
| Update an existing database while preserving business data | A new incremental migration, then normal startup | Do not rely on editing an already-applied branch or use reset. |
|
|
199
206
|
|
|
200
|
-
|
|
207
|
+
For example, from the repository root:
|
|
201
208
|
|
|
202
209
|
```bash
|
|
203
|
-
npm run test
|
|
204
|
-
npm run db:reset
|
|
210
|
+
npm run test -- 'src/module/<module-name>/test/**/*.ts'
|
|
205
211
|
```
|
|
206
212
|
|
|
207
|
-
|
|
213
|
+
Resolve the actual path for suite-owned modules. Select tests that cover the affected migration and contract behavior; expand coverage according to actual impact, not merely the presence of a `meta.version.ts` edit. Fresh-database preparation proves the fresh-install path, not every existing-data upgrade; verify upgrade behavior when the change requires it. See [Unit Testing](/backend/unit-testing#execute-tests).
|
|
214
|
+
|
|
215
|
+
The replay rule above concerns positive-numbered migration branches. The special version `0` and `fileVersion: -1` lifecycle paths retain their always-run semantics; they are not a reason to move a versioned schema change into an always-run branch.
|
|
208
216
|
|
|
209
|
-
|
|
210
|
-
- `db:reset` requires `database.testDatabase: true` in dev config and a connection outside the disposable database; it refuses to reset a configured non-disposable database
|
|
211
|
-
- use `test` when you need **test-mode** verification of migration, controller behavior, and the broader generated contract thread; E2E also remains test-mode
|
|
217
|
+
`db:reset` requires explicit approval, `database.testDatabase: true` in dev config, and a connection outside the disposable database. It refuses to reset a configured non-disposable database. Follow the lifecycle and concurrency precautions below before an approved reset.
|
|
212
218
|
|
|
213
219
|
### Managed development database preparation
|
|
214
220
|
|
|
@@ -239,7 +245,8 @@ Also ask:
|
|
|
239
245
|
|
|
240
246
|
1. does this change require a `fileVersion` increment?
|
|
241
247
|
2. does `meta.version` need an `update`, `init`, or `seed` branch?
|
|
242
|
-
3.
|
|
243
|
-
4. does the
|
|
248
|
+
3. which managed focused tests verify the affected migration and resource behavior, and does the actual impact require broader coverage?
|
|
249
|
+
4. does the development database need pending migrations through startup, a separately approved disposable reset, or a new data-preserving migration?
|
|
250
|
+
5. does the change affect the CRUD-generated thread or frontend-facing API contract as well?
|
|
244
251
|
|
|
245
252
|
That prevents schema changes from being documented in code but never integrated into the module lifecycle.
|
|
@@ -19,7 +19,7 @@ Use this page when you are contributing backend work in the framework repository
|
|
|
19
19
|
| Name | Version |
|
|
20
20
|
| ---------- | --------- |
|
|
21
21
|
| pnpm | >=12.14.0 |
|
|
22
|
-
| Node.js | >=24.
|
|
22
|
+
| Node.js | >=24.11.0 |
|
|
23
23
|
| Redis | >=7.2.6 |
|
|
24
24
|
| Sqlite3 | Built-in |
|
|
25
25
|
| MySQL | >=8 |
|
|
@@ -61,7 +61,7 @@ When the user decides not to create new migration history:
|
|
|
61
61
|
2. fold the schema change into the current version path;
|
|
62
62
|
3. do not create a new migration branch.
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Normal startup does not replay an already-applied positive-numbered migration branch. Managed focused tests exercise the edited path on an isolated test database, not the development database. A no-bump decision does not authorize a destructive dev reset; use a new incremental migration when existing data must be preserved. See [Migration and Changes](/backend/migration-and-changes#local-development-workflow) for the verification and development-database decision paths.
|
|
65
65
|
|
|
66
66
|
## Inspect the existing resource thread
|
|
67
67
|
|
|
@@ -195,7 +195,8 @@ Choose checks that match the layers changed:
|
|
|
195
195
|
|
|
196
196
|
- inspect generated schema/OpenAPI after structure-shaping field changes;
|
|
197
197
|
- run the narrow resource test and relevant typecheck;
|
|
198
|
-
-
|
|
198
|
+
- verify affected migration, initialization, or seed behavior through managed isolated tests, preferably focused; focused runs still prepare a fresh test database, so a `meta.version.ts` edit alone does not require the full suite;
|
|
199
|
+
- decide development-database updates separately: tests do not update dev data, pending migrations apply through normal startup, and replaying an already-applied positive-numbered branch on disposable dev data requires explicit reset approval;
|
|
199
200
|
- run the relevant frontend metadata/build/dependency synchronization when custom renderer resources are involved;
|
|
200
201
|
- verify action-level serializer behavior with an API test when `v.serializer*` metadata changes returned fields;
|
|
201
202
|
- 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.
|
|
@@ -8,7 +8,7 @@ Keep testing proportional to the backend change: cover critical business behavio
|
|
|
8
8
|
|
|
9
9
|
Test-driven development and targeted unit or framework tests remain useful when they clarify and directly prove new or changed behavior. Exhaustive upfront mock/fake matrices and bespoke harnesses are not a delivery prerequisite.
|
|
10
10
|
|
|
11
|
-
Preserve applicable authentication/authorization, tenant-isolation, transaction, concurrency, idempotency, migration, and acceptance checks without turning them into an unrelated mandatory matrix. The context, fixture, and resource-isolation rules below still apply,
|
|
11
|
+
Preserve applicable authentication/authorization, tenant-isolation, transaction, concurrency, idempotency, migration, and acceptance checks without turning them into an unrelated mandatory matrix. The context, fixture, and resource-isolation rules below still apply. Verify changes to migration, initialization, or seed behavior through managed isolated tests covering the affected behavior, preferably focused; a `meta.version.ts` edit alone does not require the full suite. Missing runtime access must be reported, not worked around with unauthorized resets or substitute test machinery.
|
|
12
12
|
|
|
13
13
|
Vona’s testing story is valuable because it is closely integrated with:
|
|
14
14
|
|
|
@@ -42,7 +42,13 @@ npm run vona :create:test student -- --module=training-student
|
|
|
42
42
|
|
|
43
43
|
## Execute tests
|
|
44
44
|
|
|
45
|
-
From the root
|
|
45
|
+
From the repository root, prefer a focused run for a bounded module change:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm run test -- 'src/module/<module-name>/test/**/*.ts'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Resolve the actual test path for suite-owned modules. Positional patterns select test files, not a reduced database-preparation lifecycle. Focused managed tests still exercise fresh migration, initialization, and seeds; assert the affected resource behavior and cover existing-data upgrades when applicable. Expand to the full suite according to actual impact:
|
|
46
52
|
|
|
47
53
|
```bash
|
|
48
54
|
npm run test
|
|
@@ -57,7 +63,7 @@ A typical Vona test flow includes:
|
|
|
57
63
|
5. run the test files
|
|
58
64
|
6. close the shared application after the test run
|
|
59
65
|
|
|
60
|
-
In Cabloy Basic and Cabloy Start, root `npm run test` delegates to `vona/`'s `npm run test`; both allocate an isolated test identity, database and Redis namespace, Vona generated/runtime/public paths, and loopback listener. They can run in the same checkout as an ordinary development server without resetting or mutating that server's business data. Even a targeted run performs the test-mode setup. The Vona `npm run cov` script uses the same isolation; raw `:bin:test` commands do not. Shared build/dependency preparation may still write to shared artifacts, and custom external resources need separate isolation. See [Repo Scripts](/reference/repo-scripts#test-isolation-and-development-data).
|
|
66
|
+
In Cabloy Basic and Cabloy Start, root `npm run test` delegates to `vona/`'s `npm run test`; both allocate an isolated test identity, database and Redis namespace, Vona generated/runtime/public paths, and loopback listener. They can run in the same checkout as an ordinary development server without resetting or mutating that server's business data. Even a targeted run performs the test-mode setup. Passing tests do not update the development database; choose its migration or explicitly approved reset path separately using [Migration and Changes](/backend/migration-and-changes#local-development-workflow). The Vona `npm run cov` script uses the same isolation; raw `:bin:test` commands do not. Shared build/dependency preparation may still write to shared artifacts, and custom external resources need separate isolation. See [Repo Scripts](/reference/repo-scripts#test-isolation-and-development-data).
|
|
61
67
|
|
|
62
68
|
The runner owns this application lifecycle. Test files import the shared app from `vona-mock` and must not create or close it themselves:
|
|
63
69
|
|
|
@@ -343,6 +343,38 @@ A practical rule is:
|
|
|
343
343
|
- let the cell resource own the render interaction
|
|
344
344
|
- let the command bean or model layer own the actual action semantics
|
|
345
345
|
|
|
346
|
+
### Collect input for a business row action in a dialog
|
|
347
|
+
|
|
348
|
+
When a row action needs additional business input, such as shipment details or a stock-adjustment reason, prefer a compact button in the cell and collect the input in a focused dialog. Keep deliberate inline-editing workflows inline; this is not a requirement to put every table action in a dialog.
|
|
349
|
+
|
|
350
|
+
Use the following interaction boundaries:
|
|
351
|
+
|
|
352
|
+
1. **Open from the target row.** Keep the cell responsible for the trigger and row context. For caller-owned content, use the ordinary `this.$appModal.dialog(...)` with `slotDefault` and `slotActions`; use a routed dialog only when the workflow needs routed pages. See [Routed Dialog Guide](/frontend/routed-dialog-guide#choose-the-right-primitive).
|
|
353
|
+
2. **Collect operation-specific input.** Prefer the operation's generated `$apiSchema` or the owning `ModelResource` schema with automatic `ZForm` rendering when the contract already describes the fields. Keep localized schema access reactive, and do not copy backend field metadata into the controller. Use manual or mixed fields only for a concrete interaction need, such as UI-only confirmation. See [API Schema Guide](/frontend/api-schema-guide#direct-schema-driven-form-pattern) and [Form Guide](/frontend/form-guide#preserve-the-automatic-custom-rendering-boundary).
|
|
354
|
+
3. **Submit through the existing action owner.** Keep the draft local to the interaction, but reuse the existing command or semantic model facade for business execution. For an action on the same resource, retain its existing `ModelResource` mutation and cache owner rather than adding dialog-local fetch/cache state or manual list refreshes. See [Resource Model Cookbook](/frontend/model-resource-cookbook#recipe-4-inside-that-facade-add-a-row-level-custom-mutation).
|
|
355
|
+
4. **Keep one complete submission lifecycle.** Validate and normalize input as required by the operation, and prevent duplicate submission at the boundary that owns all submit paths. Return or await the whole action through `ZButton.onPerform` for a button-only action; use form submission state and error handling when `ZForm` owns submission. Reflect broader mutation pending state only when it is needed beyond that button's own lifecycle. Choose one error presentation owner, not competing alerts. See [Behavior Guide](/frontend/behavior-guide#perform-a-standalone-asynchronous-button-action).
|
|
356
|
+
5. **Close only after success.** Await the mutation before calling the dialog's `close()`. On failure, keep the dialog and entered values available for correction or retry; do not close in `finally`. Cancel or dismissal must not initiate a mutation, but closing the dialog does not cancel an already submitted request. Choose confirmation, draft reset/recovery, and dismissal behavior from the business workflow rather than copying one example's checkbox, width, or reset policy.
|
|
357
|
+
|
|
358
|
+
For a schema-driven form, keep its submit controls in `ZForm.slotFooter` when appropriate so that validation and native-form submission remain one path. The dialog's `slotActions` is not a reason to introduce a second submission owner.
|
|
359
|
+
|
|
360
|
+
Apply these schema-driven form details where they fit the operation:
|
|
361
|
+
|
|
362
|
+
- **Submit same-shaped data directly.** When the validated form data has the same shape as the operation's request DTO, pass `data.value` directly to the existing mutation, for example `await mutation.mutateAsync(data.value)`. Build a separate payload only when the operation needs a real projection, conversion, or removal of UI-only fields. Do not enumerate fields or duplicate normalization merely to reconstruct the same request; when the backend already owns normalization, leave it there. Schema validation does not imply that every operation trims its input.
|
|
363
|
+
- **Await business confirmation inside `onSubmitData`.** If the operation requires a confirmation prompt, await it after form validation and before the mutation in the same returned or awaited submit handler. Button submission and Enter then share validation, confirmation, pending state, and error handling. A declined confirmation should return without mutating or closing the form, preserving the draft. Do not put confirmation only in the submit button's click handler or introduce a confirmation checkbox unless the business workflow calls for one.
|
|
364
|
+
- **Prepare lazy schemas at the opening boundary when appropriate.** For a low-frequency dialog, opening can await `$QueryEnsureLoaded(() => this.apiSchemasShip.sdk)` before rendering the form. Resolve `apiSchemasShip` through a getter for the current runtime and derive `requestBody` reactively, such as with `$computed`. Ensure the current locale's SDK on every opening, even if the reactive derivation was installed earlier; do not retain a facade or schema snapshot from a previous locale. If preparation is extracted into a helper, await SDK readiness on every call and use `if (!this.schema)` only to create the computed derivation once. Do not place `if (this.schema) return` before the SDK check: an existing computed does not prove that the current locale's SDK is loaded. Extracting a helper is optional; the important boundary is repeated readiness checks versus one-time derivation setup. This is an optional loading boundary, not a requirement to defer every form's schema until interaction.
|
|
365
|
+
|
|
366
|
+
The Cabloy Basic shipment example is `zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionShip/controller.tsx`: `_openDialog()` calls `_ensureSchema()` to prepare the current schema, `ZForm` renders the contract fields automatically, and `_submit()` awaits confirmation and `mutateAsync(data.value)` before closing. Its dialog width, draft-reset policy, and backend normalization are operation-specific choices, not additional form rules.
|
|
367
|
+
|
|
368
|
+
Extend an existing critical-flow E2E scenario rather than creating a separate harness:
|
|
369
|
+
|
|
370
|
+
- locate the trigger within the intended order or resource row, then locate fields and submit controls within the opened dialog
|
|
371
|
+
- check that the cell no longer exposes the dialog's input fields, and that invalid input cannot submit
|
|
372
|
+
- check cancellation and the chosen reopen/draft policy
|
|
373
|
+
- check successful submission, dialog closure, and the refreshed resource state
|
|
374
|
+
- when covering a submission failure, assert that the dialog retains the draft, presents the intended error, and allows retry after pending state clears
|
|
375
|
+
|
|
376
|
+
In Cabloy Basic, the ordinary dialog can be located with `getByRole('dialog')`; assert its visible heading rather than assuming the title also supplies its accessible name. Verify the active edition's modal adapter before reusing UI-specific selectors.
|
|
377
|
+
|
|
346
378
|
## Pattern 5: Row-action composition with `tableActionRow`
|
|
347
379
|
|
|
348
380
|
Use this pattern when one cell should show several row actions together.
|
|
@@ -9,7 +9,7 @@ Before creating a new Cabloy project, make sure your environment has:
|
|
|
9
9
|
| Name | Version |
|
|
10
10
|
| ------------ | ----------- |
|
|
11
11
|
| `pnpm` | `>=12.14.0` |
|
|
12
|
-
| `Node.js` | `>=24.
|
|
12
|
+
| `Node.js` | `>=24.11.0` |
|
|
13
13
|
| `Redis` | `>=7.2.6` |
|
|
14
14
|
| `SQLite3` | `Built-in` |
|
|
15
15
|
| `MySQL` | `>=8` |
|
package/repo-docs/pnpm-lock.yaml
CHANGED
|
@@ -697,8 +697,8 @@ packages:
|
|
|
697
697
|
shiki@2.5.0:
|
|
698
698
|
resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==}
|
|
699
699
|
|
|
700
|
-
source-map-js@1.2.
|
|
701
|
-
resolution: {integrity: sha512-
|
|
700
|
+
source-map-js@1.2.2:
|
|
701
|
+
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
|
|
702
702
|
engines: {node: '>=0.10.0'}
|
|
703
703
|
|
|
704
704
|
space-separated-tokens@2.0.2:
|
|
@@ -1175,7 +1175,7 @@ snapshots:
|
|
|
1175
1175
|
'@vue/shared': 3.5.35
|
|
1176
1176
|
entities: 7.0.1
|
|
1177
1177
|
estree-walker: 2.0.2
|
|
1178
|
-
source-map-js: 1.2.
|
|
1178
|
+
source-map-js: 1.2.2
|
|
1179
1179
|
|
|
1180
1180
|
'@vue/compiler-dom@3.5.35':
|
|
1181
1181
|
dependencies:
|
|
@@ -1192,7 +1192,7 @@ snapshots:
|
|
|
1192
1192
|
estree-walker: 2.0.2
|
|
1193
1193
|
magic-string: 0.30.21
|
|
1194
1194
|
postcss: 8.5.15
|
|
1195
|
-
source-map-js: 1.2.
|
|
1195
|
+
source-map-js: 1.2.2
|
|
1196
1196
|
|
|
1197
1197
|
'@vue/compiler-ssr@3.5.35':
|
|
1198
1198
|
dependencies:
|
|
@@ -1425,7 +1425,7 @@ snapshots:
|
|
|
1425
1425
|
dependencies:
|
|
1426
1426
|
nanoid: 3.3.12
|
|
1427
1427
|
picocolors: 1.1.1
|
|
1428
|
-
source-map-js: 1.2.
|
|
1428
|
+
source-map-js: 1.2.2
|
|
1429
1429
|
|
|
1430
1430
|
preact@10.29.2: {}
|
|
1431
1431
|
|
|
@@ -1487,7 +1487,7 @@ snapshots:
|
|
|
1487
1487
|
'@shikijs/vscode-textmate': 10.0.2
|
|
1488
1488
|
'@types/hast': 3.0.4
|
|
1489
1489
|
|
|
1490
|
-
source-map-js@1.2.
|
|
1490
|
+
source-map-js@1.2.2: {}
|
|
1491
1491
|
|
|
1492
1492
|
space-separated-tokens@2.0.2: {}
|
|
1493
1493
|
|
|
@@ -74,7 +74,7 @@ Root `npm run tsc` runs `tsc:zova`, then `tsc:vona`; these are the only root `ts
|
|
|
74
74
|
|
|
75
75
|
In both nested workspaces, `npm run tsc` delegates to `npm run tsc:native`. Native and legacy checks first run `npm run prerun`, then invoke the selected compiler with incremental caching disabled. Root workspace aliases delegate to these scripts too, so dependency and generated metadata preparation is not bypassed. Each workspace's `prerun` runs only its `:tools:deps` command; it does not call `tsc`, avoiding recursive script execution. Preparation may refresh generated metadata or dependency manifests and install changed dependencies, while the compiler step itself emits no declarations or build-info files.
|
|
76
76
|
|
|
77
|
-
Zova development and build entrypoints call `npm run prerun` before Quasar or REST emission to prepare dependencies and generated metadata without automatically running a typecheck. Run `npm run tsc`, `npm run tsc:native`, or `npm run tsc:legacy` explicitly inside the Zova workspace when source typechecking is needed. The former multi-project Vona CLI command `:bin:tsc` has been removed; use the project checking scripts instead. Module publication configurations
|
|
77
|
+
Zova development and build entrypoints call `npm run prerun` before Quasar or REST emission to prepare dependencies and generated metadata without automatically running a typecheck. Run `npm run tsc`, `npm run tsc:native`, or `npm run tsc:legacy` explicitly inside the Zova workspace when source typechecking is needed. The former multi-project Vona CLI command `:bin:tsc` has been removed; use the project checking scripts instead. Module publication configurations retain their existing compiler selection. REST emission uses tsdown 0.23.1 with an explicit JavaScript-only pass (`dts: false`) followed by a declaration-only pass (`dts.generator: 'tsgo'`). The frontend CLI and fullstack workflows require Node.js `>=24.11.0`. Classic TypeScript remains installed for compiler-JavaScript-API consumers, including SWC configuration loading, Rollup integration, and OpenAPI generation; the REST emitter retains the `@typescript/native-preview` dependency name as an npm alias for stable `typescript@7.0.2`, preserving its declaration plugin's compiler lookup.
|
|
78
78
|
|
|
79
79
|
The native alias is installed at the repository root, not in either nested workspace. Run `pnpm install` at the root after updating the checkout; a missing alias fails with an installation diagnostic rather than silently selecting the legacy compiler. Do not use a bare root `tsc`/`pnpm exec tsc` to choose a compiler, because both packages declare the same binary name.
|
|
80
80
|
|
|
@@ -208,7 +208,7 @@ Combine producer stdout/stderr **before** sanitization, never `tee` or spool raw
|
|
|
208
208
|
|
|
209
209
|
### Windows and PowerShell
|
|
210
210
|
|
|
211
|
-
The tool uses Node's native filesystem APIs on Windows, Linux and macOS;
|
|
211
|
+
The tool uses Node's native filesystem APIs on Windows, Linux and macOS; use a Node.js version supported by the repository's root `engines.node` declaration. Path-text recognition is cross-platform independently of the host OS. Native filesystem behavior is checked by the `evidence-platforms` CI matrix on Ubuntu, Windows and macOS. A configured matrix is not a successful run: inspect the actual job results before claiming all three platforms are verified.
|
|
212
212
|
|
|
213
213
|
From the repository root, PowerShell can invoke the CLI directly:
|
|
214
214
|
|
|
@@ -936,22 +936,96 @@ test(
|
|
|
936
936
|
await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
|
|
937
937
|
await adminPage.getByRole('button', { name: 'Back', exact: true }).click();
|
|
938
938
|
await expect(orderRow).toBeVisible();
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
939
|
+
const openShipmentButton = orderRow.getByRole('button', {
|
|
940
|
+
name: 'Ship order',
|
|
941
|
+
exact: true,
|
|
942
|
+
});
|
|
943
|
+
await expect(openShipmentButton).toHaveCount(1);
|
|
944
|
+
await expect(orderRow.locator('input')).toHaveCount(0);
|
|
945
|
+
await openShipmentButton.click();
|
|
946
|
+
const shipmentDialog = adminPage.getByRole('dialog').filter({
|
|
947
|
+
has: adminPage.getByRole('heading', { name: 'Ship order', exact: true }),
|
|
948
|
+
});
|
|
949
|
+
const shipmentConfirmation = adminPage.getByRole('dialog').filter({
|
|
950
|
+
hasText: 'Ship this order with the entered shipment details?',
|
|
951
|
+
});
|
|
952
|
+
await expect(shipmentDialog).toBeVisible();
|
|
953
|
+
const carrierField = shipmentDialog.locator('fieldset').filter({
|
|
954
|
+
has: adminPage.locator('legend').filter({ hasText: /^Carrier$/ }),
|
|
955
|
+
});
|
|
956
|
+
const trackingNumberField = shipmentDialog.locator('fieldset').filter({
|
|
957
|
+
has: adminPage.locator('legend').filter({ hasText: /^Tracking number$/ }),
|
|
958
|
+
});
|
|
959
|
+
const carrierInput = carrierField.getByRole('textbox');
|
|
960
|
+
const trackingNumberInput = trackingNumberField.getByRole('textbox');
|
|
961
|
+
await expect(shipmentDialog.getByRole('checkbox')).toHaveCount(0);
|
|
962
|
+
const submitShipmentButton = shipmentDialog.getByRole('button', {
|
|
963
|
+
name: 'Ship order',
|
|
964
|
+
exact: true,
|
|
965
|
+
});
|
|
966
|
+
let shipmentRequestCount = 0;
|
|
967
|
+
adminPage.on('request', request => {
|
|
968
|
+
if (
|
|
969
|
+
request.method() === 'POST' &&
|
|
970
|
+
new URL(request.url()).pathname === `/api/commerce/trade/order/${checkout.orderId}/ship`
|
|
971
|
+
) {
|
|
972
|
+
shipmentRequestCount++;
|
|
973
|
+
}
|
|
974
|
+
});
|
|
942
975
|
await carrierInput.fill(shipmentCarrier);
|
|
943
976
|
await trackingNumberInput.fill(shipmentTrackingNumber);
|
|
944
|
-
await
|
|
945
|
-
await expect(
|
|
946
|
-
await expect(
|
|
947
|
-
await
|
|
977
|
+
await shipmentDialog.getByRole('button', { name: 'Cancel', exact: true }).click();
|
|
978
|
+
await expect(shipmentDialog).toHaveCount(0);
|
|
979
|
+
await expect(orderRow).toContainText('Paid');
|
|
980
|
+
await openShipmentButton.click();
|
|
981
|
+
await expect(carrierInput).toHaveValue('');
|
|
982
|
+
await expect(trackingNumberInput).toHaveValue('');
|
|
983
|
+
await submitShipmentButton.click();
|
|
984
|
+
await expect(carrierField.locator('.text-error')).toBeVisible();
|
|
985
|
+
await expect(trackingNumberField.locator('.text-error')).toBeVisible();
|
|
986
|
+
await expect(shipmentConfirmation).toHaveCount(0);
|
|
987
|
+
await carrierInput.fill(' ');
|
|
988
|
+
await trackingNumberInput.fill(shipmentTrackingNumber);
|
|
989
|
+
await submitShipmentButton.click();
|
|
990
|
+
await expect(carrierField.locator('.text-error')).toBeVisible();
|
|
991
|
+
await expect(shipmentConfirmation).toHaveCount(0);
|
|
992
|
+
await carrierInput.fill('x'.repeat(101));
|
|
993
|
+
await submitShipmentButton.click();
|
|
994
|
+
await expect(carrierField.locator('.text-error')).toBeVisible();
|
|
995
|
+
await expect(shipmentConfirmation).toHaveCount(0);
|
|
996
|
+
await carrierInput.fill(` ${shipmentCarrier} `);
|
|
997
|
+
await trackingNumberInput.fill('x'.repeat(101));
|
|
998
|
+
await submitShipmentButton.click();
|
|
999
|
+
await expect(trackingNumberField.locator('.text-error')).toBeVisible();
|
|
1000
|
+
await expect(shipmentConfirmation).toHaveCount(0);
|
|
1001
|
+
await trackingNumberInput.fill(` ${shipmentTrackingNumber} `);
|
|
1002
|
+
await submitShipmentButton.click();
|
|
1003
|
+
await expect(shipmentConfirmation).toBeVisible();
|
|
1004
|
+
await expect(submitShipmentButton).toBeDisabled();
|
|
1005
|
+
expect(shipmentRequestCount).toBe(0);
|
|
1006
|
+
await shipmentConfirmation.getByRole('button', { name: 'No', exact: true }).click();
|
|
1007
|
+
await expect(shipmentConfirmation).toHaveCount(0);
|
|
1008
|
+
await expect(carrierInput).toHaveValue(` ${shipmentCarrier} `);
|
|
1009
|
+
await expect(trackingNumberInput).toHaveValue(` ${shipmentTrackingNumber} `);
|
|
1010
|
+
await expect(orderRow).toContainText('Paid');
|
|
1011
|
+
expect(shipmentRequestCount).toBe(0);
|
|
1012
|
+
await expect(submitShipmentButton).toBeEnabled();
|
|
1013
|
+
await trackingNumberInput.press('Enter');
|
|
1014
|
+
await expect(shipmentConfirmation).toBeVisible();
|
|
948
1015
|
const shipmentResponse = waitForApiResponse(
|
|
949
1016
|
adminPage,
|
|
950
1017
|
'POST',
|
|
951
1018
|
`/api/commerce/trade/order/${checkout.orderId}/ship`,
|
|
952
1019
|
);
|
|
953
|
-
await
|
|
954
|
-
|
|
1020
|
+
await shipmentConfirmation.getByRole('button', { name: 'Yes', exact: true }).click();
|
|
1021
|
+
const shipmentResult = await shipmentResponse;
|
|
1022
|
+
expect(shipmentResult.ok()).toBeTruthy();
|
|
1023
|
+
expect(shipmentResult.request().postDataJSON()).toEqual({
|
|
1024
|
+
carrier: ` ${shipmentCarrier} `,
|
|
1025
|
+
trackingNumber: ` ${shipmentTrackingNumber} `,
|
|
1026
|
+
});
|
|
1027
|
+
expect(shipmentRequestCount).toBe(1);
|
|
1028
|
+
await expect(shipmentDialog).toHaveCount(0);
|
|
955
1029
|
await expect(orderRow).toContainText('Shipped');
|
|
956
1030
|
expect(adminPageErrors).toEqual([]);
|
|
957
1031
|
} finally {
|
package/scripts/init.ts
CHANGED
|
@@ -19,6 +19,7 @@ const ROOT_DIR = resolve(__dirname, '..');
|
|
|
19
19
|
const VONA_DIR = resolve(ROOT_DIR, 'vona');
|
|
20
20
|
const ZOVA_DIR = resolve(ROOT_DIR, 'zova');
|
|
21
21
|
const REPO_DOCS_DIR = resolve(ROOT_DIR, 'repo-docs');
|
|
22
|
+
const NODE_VERSION = '24.11.0';
|
|
22
23
|
const PNPM_VERSION = '12.14.0';
|
|
23
24
|
const VERSION_MARKER_FILE = '.cabloy-version';
|
|
24
25
|
|
|
@@ -50,6 +51,18 @@ function pnpmInstall(cwd = ROOT_DIR): void {
|
|
|
50
51
|
exec('pnpm install --config.confirmModulesPurge=false --no-frozen-lockfile', cwd);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function checkNode(): void {
|
|
55
|
+
const version = process.versions.node;
|
|
56
|
+
const current = version.split('.').map(Number);
|
|
57
|
+
const minimum = NODE_VERSION.split('.').map(Number);
|
|
58
|
+
for (let i = 0; i < minimum.length; i++) {
|
|
59
|
+
if (current[i] > minimum[i]) return;
|
|
60
|
+
if (current[i] < minimum[i]) {
|
|
61
|
+
throw new Error(`Node.js should >= ${NODE_VERSION}, current: ${version}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
function checkPnpm(): void {
|
|
54
67
|
const version = execQuiet('pnpm --version').trimEnd();
|
|
55
68
|
const [major, minor] = version.split('.').map(item => Number.parseInt(item, 10) || 0);
|
|
@@ -295,6 +308,7 @@ function initRepoDocs(): void {
|
|
|
295
308
|
|
|
296
309
|
// --- Main ---
|
|
297
310
|
|
|
311
|
+
checkNode();
|
|
298
312
|
checkPnpm();
|
|
299
313
|
exec('node scripts/bootstrapAgentGovernance.mjs');
|
|
300
314
|
pnpmInstall();
|