cabloy 5.1.227 → 5.1.228
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 +16 -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 +84 -13
- package/repo-agent-governance/tools/evidence-paths/sanitize.mjs +38 -8
- 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/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
|
@@ -9,14 +9,15 @@ export const LINE_LIMIT = 1024 * 1024;
|
|
|
9
9
|
export const FILE_LIMIT = 8 * 1024 * 1024;
|
|
10
10
|
|
|
11
11
|
class SafeError extends Error {
|
|
12
|
-
constructor(code) {
|
|
12
|
+
constructor(code, details) {
|
|
13
13
|
super(code);
|
|
14
14
|
this.code = code;
|
|
15
|
+
this.details = details;
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
function fail(code) {
|
|
19
|
-
throw new SafeError(code);
|
|
19
|
+
function fail(code, details) {
|
|
20
|
+
throw new SafeError(code, details);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function rootInfo(value) {
|
|
@@ -247,6 +248,7 @@ export async function sanitizeStream(input, output, { matcher = createMatcher()
|
|
|
247
248
|
output.on('error', onError);
|
|
248
249
|
let parts = [];
|
|
249
250
|
let length = 0;
|
|
251
|
+
let lineNumber = 1;
|
|
250
252
|
try {
|
|
251
253
|
for await (const chunk of input) {
|
|
252
254
|
if (outputFailed) fail('OUTPUT_ERROR');
|
|
@@ -262,9 +264,10 @@ export async function sanitizeStream(input, output, { matcher = createMatcher()
|
|
|
262
264
|
parts.push(bytes.subarray(start, end));
|
|
263
265
|
if (newline < 0) break;
|
|
264
266
|
const line = Buffer.concat(parts, length);
|
|
265
|
-
|
|
267
|
+
rejectBinary(line, lineNumber);
|
|
266
268
|
const text = decode(line);
|
|
267
269
|
await write(output, `${matcher.sanitize(text)}\n`);
|
|
270
|
+
lineNumber++;
|
|
268
271
|
parts = [];
|
|
269
272
|
length = 0;
|
|
270
273
|
start = newline + 1;
|
|
@@ -272,7 +275,7 @@ export async function sanitizeStream(input, output, { matcher = createMatcher()
|
|
|
272
275
|
}
|
|
273
276
|
if (length) {
|
|
274
277
|
const line = Buffer.concat(parts, length);
|
|
275
|
-
|
|
278
|
+
rejectBinary(line, lineNumber);
|
|
276
279
|
await write(output, matcher.sanitize(decode(line)));
|
|
277
280
|
}
|
|
278
281
|
if (outputFailed) fail('OUTPUT_ERROR');
|
|
@@ -293,12 +296,21 @@ function safeFilename(value, matcher) {
|
|
|
293
296
|
);
|
|
294
297
|
}
|
|
295
298
|
|
|
296
|
-
function
|
|
297
|
-
return bytes.
|
|
299
|
+
function binaryIndex(bytes) {
|
|
300
|
+
return bytes.findIndex(
|
|
298
301
|
byte => byte === 0 || byte === 127 || (byte < 32 && ![9, 10, 12, 13].includes(byte)),
|
|
299
302
|
);
|
|
300
303
|
}
|
|
301
304
|
|
|
305
|
+
function binary(bytes) {
|
|
306
|
+
return binaryIndex(bytes) >= 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function rejectBinary(bytes, line) {
|
|
310
|
+
const index = binaryIndex(bytes);
|
|
311
|
+
if (index >= 0) fail('BINARY', { byte: bytes[index], line, bytePosition: index + 1 });
|
|
312
|
+
}
|
|
313
|
+
|
|
302
314
|
async function ancestorExclusion(path) {
|
|
303
315
|
const root = parse(path).root;
|
|
304
316
|
let current = root;
|
|
@@ -465,6 +477,9 @@ paths and unknown absolute roots are left unchanged: this is NOT a universal sec
|
|
|
465
477
|
scanner. Use explicit maps for other local roots; inspect output before publication.
|
|
466
478
|
UTF-8 decoding is fatal, with a 1 MiB byte limit per line (including CR, excluding LF).
|
|
467
479
|
NUL, DEL and non-text C0 controls are rejected; tab, LF, form feed and CR are allowed.
|
|
480
|
+
sanitize BINARY diagnostics report only the first rejected decimal byte, 1-based input
|
|
481
|
+
line and byte-position within that original UTF-8 line. LF advances the line; CR counts
|
|
482
|
+
as a byte, not a line boundary. No text from the rejected line is reported.
|
|
468
483
|
Only complete validated lines are emitted; earlier sanitized lines may survive failure.
|
|
469
484
|
check limits each file to 8 MiB, treats NUL/non-text controls as binary, and reports only
|
|
470
485
|
scope-N/relative-filename:line CODE, never source lines or absolute scan roots.
|
|
@@ -530,8 +545,23 @@ export async function runCli(
|
|
|
530
545
|
return result.status;
|
|
531
546
|
} catch (caught) {
|
|
532
547
|
const code = caught instanceof SafeError ? caught.code : 'INTERNAL_ERROR';
|
|
548
|
+
let diagnostic = code;
|
|
549
|
+
if (caught instanceof SafeError && code === 'BINARY') {
|
|
550
|
+
const { byte, line, bytePosition } = caught.details ?? {};
|
|
551
|
+
if (
|
|
552
|
+
Number.isInteger(byte) &&
|
|
553
|
+
byte >= 0 &&
|
|
554
|
+
byte <= 255 &&
|
|
555
|
+
Number.isSafeInteger(line) &&
|
|
556
|
+
line > 0 &&
|
|
557
|
+
Number.isSafeInteger(bytePosition) &&
|
|
558
|
+
bytePosition > 0
|
|
559
|
+
) {
|
|
560
|
+
diagnostic += ` byte=${byte} line=${line} byte-position=${bytePosition}`;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
533
563
|
try {
|
|
534
|
-
await write(error, `evidence-paths: ${
|
|
564
|
+
await write(error, `evidence-paths: ${diagnostic}\n`);
|
|
535
565
|
} catch {
|
|
536
566
|
/* no unsafe fallback */
|
|
537
567
|
}
|
|
@@ -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` |
|
|
@@ -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();
|