cabloy 5.1.211 → 5.1.213

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/package.json +1 -1
  4. package/repo-agent-governance/managed-assets.json +3 -3
  5. package/repo-agent-governance/skills/cabloy-contract-loop/SKILL.md +2 -2
  6. package/repo-docs/frontend/permission-formscene-action-visibility-guide.md +7 -5
  7. package/vona/packages-cli/cli/package.json +1 -1
  8. package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/<%=argv.detailDtoResItemName%>.tsx_ +3 -3
  9. package/vona/packages-cli/cli-set-api/package.json +1 -1
  10. package/vona/src/suite/a-training/modules/training-record/src/dto/detailRecordSubjectResItem.tsx +3 -3
  11. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordResItem.tsx +3 -3
  12. package/zova/packages-zova/zova/package.json +2 -2
  13. package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionOperationsRow.tsx +6 -5
  14. package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionUpdate.tsx +1 -0
  15. package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionView.tsx +1 -0
  16. package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/actionCreate/controller.tsx +1 -0
  17. package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/formFieldDetails/controller.tsx +20 -8
  18. package/zova/src/suite/cabloy-basic/modules/basic-details/src/lib/detailsPermissions.ts +6 -4
  19. package/zova/src/suite/cabloy-basic/modules/basic-details/src/service/detail.tsx +1 -0
  20. package/zova/src/suite/cabloy-basic/modules/basic-details/src/types/detail.ts +2 -0
  21. package/zova/src/suite/cabloy-basic/modules/basic-details/src/types/details.ts +2 -2
  22. package/zova/src/suite/cabloy-basic/modules/basic-details/src/types/dialogForm.ts +3 -0
  23. package/zova/src/suite/cabloy-basic/modules/basic-details/test/lib/detailsPermissions.test.ts +196 -1
  24. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  25. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/permissions.ts +8 -2
  26. package/zova/src/suite-vendor/a-zova/package.json +2 -2
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.211
1
+ 5.1.213
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.213
4
+
5
+ ### Bug Fixes
6
+
7
+ - Fix details.
8
+
9
+ ## 5.1.212
10
+
11
+ ### Features
12
+
13
+ - Add OpenAPI SDK skills.
14
+
3
15
  ## 5.1.211
4
16
 
5
17
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.211",
3
+ "version": "5.1.213",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -60,7 +60,7 @@
60
60
  {
61
61
  "adapter": "codex",
62
62
  "category": "skill",
63
- "sha256": "4aeb0477e3cb316b14bca7c4a3d1e277329be4a8f8e14ef31a3ceb858b6b0698",
63
+ "sha256": "27888ed8bb2c177a7bbe836a494d6185580f2e12e9de809809e3b38820084f94",
64
64
  "source": "skills/cabloy-contract-loop/SKILL.md",
65
65
  "target": ".agents/skills/cabloy-contract-loop/SKILL.md"
66
66
  },
@@ -396,7 +396,7 @@
396
396
  {
397
397
  "adapter": "claude",
398
398
  "category": "skill",
399
- "sha256": "4aeb0477e3cb316b14bca7c4a3d1e277329be4a8f8e14ef31a3ceb858b6b0698",
399
+ "sha256": "27888ed8bb2c177a7bbe836a494d6185580f2e12e9de809809e3b38820084f94",
400
400
  "source": "skills/cabloy-contract-loop/SKILL.md",
401
401
  "target": ".claude/skills/cabloy-contract-loop/SKILL.md"
402
402
  },
@@ -746,7 +746,7 @@
746
746
  {
747
747
  "adapter": "cursor",
748
748
  "category": "skill",
749
- "sha256": "4aeb0477e3cb316b14bca7c4a3d1e277329be4a8f8e14ef31a3ceb858b6b0698",
749
+ "sha256": "27888ed8bb2c177a7bbe836a494d6185580f2e12e9de809809e3b38820084f94",
750
750
  "source": "skills/cabloy-contract-loop/SKILL.md",
751
751
  "target": ".cursor/skills/cabloy-contract-loop/SKILL.md"
752
752
  },
@@ -220,13 +220,13 @@ Use this when the frontend consumes generated API contracts.
220
220
 
221
221
  For AI development, **default to managed isolated generation**, even when a development Swagger service is currently reachable. An independently managed development service may stop at any time; owning the service lifecycle keeps the generation task continuous and its contract source consistent.
222
222
 
223
- 1. Detect the edition and verify the isolated entrypoint in the active root `package.json`. Cabloy Basic provides:
223
+ 1. Both Cabloy Basic and Cabloy Start support managed isolated generation through the same root command:
224
224
 
225
225
  ```bash
226
226
  npm run contract:generate:isolated -- <module-name>
227
227
  ```
228
228
 
229
- For Cabloy Start or another version, verify the actual command and configuration support rather than assuming the Basic entrypoint exists.
229
+ Detect the active edition for edition-specific paths and follow-up builds, not to choose a different SDK generation default. For older project versions, verify the root entrypoint and source resolver are up to date.
230
230
 
231
231
  2. Initialize module configuration with `npm run zova :openapi:config <module-name>` when needed. Keep module ownership constrained with `operations.match` unless the module intentionally owns a broad API surface; the isolated command reuses the existing generator and ownership checks.
232
232
  3. Confirm the effective source uses the isolated service. The command supplies a child-scoped `OPENAPI_SOURCE_URL` to the project default source resolver; explicit module sources or project `modules` overrides retain their precedence. Inspect the project resolver and align conflicting sources deliberately before claiming isolated generation. Do not edit `.env` files or SDK runtime API targets to inject the temporary port.
@@ -214,11 +214,11 @@ The current source confirms:
214
214
 
215
215
  That means the details action family is scene-aware in a different way from `tableActionBulk`.
216
216
 
217
- A practical current example from backend DTO metadata is:
217
+ A practical details example from backend DTO metadata is:
218
218
 
219
219
  ```tsx
220
220
  ZovaRender.detailsActionBulk('basic-details:actionCreate', {
221
- permission: { formScene: ['create', 'edit'] },
221
+ permission: { mode: 'formScene', formScene: ['create', 'edit'] },
222
222
  });
223
223
  ```
224
224
 
@@ -226,15 +226,17 @@ and:
226
226
 
227
227
  ```tsx
228
228
  ZovaRender.detailsActionRow('basic-details:actionDelete', {
229
- permission: { formScene: ['create', 'edit'] },
229
+ permission: { mode: 'formScene', formScene: ['create', 'edit'] },
230
230
  });
231
231
  ```
232
232
 
233
+ For default detail CRUD actions, `permission.mode: 'formScene'` makes the intent explicit: the enclosing form scene controls visibility and no independent action permission check is performed. Custom detail actions should use `permission.mode: 'action'`; an explicit `actionInherit` also takes the action-checking path. If `mode` is omitted, the runtime uses the conservative action-checking path rather than inferring behavior from the action name.
234
+
233
235
  So the correct distinction is:
234
236
 
235
237
  - `tableActionBulk` does not currently support `formScene`
236
- - `detailsActionBulk` does support `formScene`
237
- - `detailsActionRow` also supports `formScene`
238
+ - `detailsActionBulk` supports `formScene` and the detail-specific `mode`
239
+ - `detailsActionRow` supports `formScene` and the detail-specific `mode`
238
240
 
239
241
  ## 7. Why bulk actions are different today
240
242
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.162",
3
+ "version": "1.1.163",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli",
6
6
  "keywords": [
@@ -19,7 +19,7 @@ export interface IDtoOptions<%=cli.helper.firstCharToUpperCase(argv.detailDtoRes
19
19
  actions: [
20
20
  ZovaRender.detailsActionBulk('<%=argv.detailsModuleName%>:actionCreate', {
21
21
  dialogOptions: { title: $locale('Add<%=argv.detailDialogTitleCapitalize%>') },
22
- permission: { formScene: ['create', 'edit'] },
22
+ permission: { mode: 'formScene', formScene: ['create', 'edit'] },
23
23
  }),
24
24
  ],
25
25
  }),
@@ -48,10 +48,10 @@ export class Dto<%=cli.helper.firstCharToUpperCase(argv.detailDtoResItemName)%>
48
48
  actions: [
49
49
  ZovaRender.detailsActionRow('<%=argv.detailsModuleName%>:actionUpdate', {
50
50
  dialogOptions: { title: $locale('Edit<%=argv.detailDialogTitleCapitalize%>') },
51
- permission: { formScene: ['create', 'edit'] },
51
+ permission: { mode: 'formScene', formScene: ['create', 'edit'] },
52
52
  }),
53
53
  ZovaRender.detailsActionRow('<%=argv.detailsModuleName%>:actionDelete', {
54
- permission: { formScene: ['create', 'edit'] },
54
+ permission: { mode: 'formScene', formScene: ['create', 'edit'] },
55
55
  }),
56
56
  ],
57
57
  }),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.160",
3
+ "version": "1.1.161",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -19,7 +19,7 @@ export interface IDtoOptionsDetailRecordSubjectResItem extends IDecoratorDtoOpti
19
19
  actions: [
20
20
  ZovaRender.detailsActionBulk('basic-details:actionCreate', {
21
21
  dialogOptions: { title: $locale('AddTrainingRecordSubject') },
22
- permission: { formScene: ['create', 'edit'] },
22
+ permission: { mode: 'formScene', formScene: ['create', 'edit'] },
23
23
  }),
24
24
  ],
25
25
  }),
@@ -53,10 +53,10 @@ export class DtoDetailRecordSubjectResItem extends $Dto.get(() => ModelSubject,
53
53
  actions: [
54
54
  ZovaRender.detailsActionRow('basic-details:actionUpdate', {
55
55
  dialogOptions: { title: $locale('EditTrainingRecordSubject') },
56
- permission: { formScene: ['create', 'edit'] },
56
+ permission: { mode: 'formScene', formScene: ['create', 'edit'] },
57
57
  }),
58
58
  ZovaRender.detailsActionRow('basic-details:actionDelete', {
59
- permission: { formScene: ['create', 'edit'] },
59
+ permission: { mode: 'formScene', formScene: ['create', 'edit'] },
60
60
  }),
61
61
  ],
62
62
  }),
@@ -19,7 +19,7 @@ export interface IDtoOptionsDetailRecordResItem extends IDecoratorDtoOptions {}
19
19
  actions: [
20
20
  ZovaRender.detailsActionBulk('basic-details:actionCreate', {
21
21
  dialogOptions: { title: $locale('AddTrainingRecord') },
22
- permission: { formScene: ['create', 'edit'] },
22
+ permission: { mode: 'formScene', formScene: ['create', 'edit'] },
23
23
  }),
24
24
  ],
25
25
  }),
@@ -54,10 +54,10 @@ export class DtoDetailRecordResItem extends $Dto.get(() => ModelRecord, {
54
54
  actions: [
55
55
  ZovaRender.detailsActionRow('basic-details:actionUpdate', {
56
56
  dialogOptions: { title: $locale('EditTrainingRecord') },
57
- permission: { formScene: ['create', 'edit'] },
57
+ permission: { mode: 'formScene', formScene: ['create', 'edit'] },
58
58
  }),
59
59
  ZovaRender.detailsActionRow('basic-details:actionDelete', {
60
- permission: { formScene: ['create', 'edit'] },
60
+ permission: { mode: 'formScene', formScene: ['create', 'edit'] },
61
61
  }),
62
62
  ],
63
63
  }),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova",
3
- "version": "5.1.170",
3
+ "version": "5.1.171",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A vue3 framework with ioc",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "zova-core": "^5.1.92",
49
- "zova-suite-a-zova": "^5.1.169"
49
+ "zova-suite-a-zova": "^5.1.170"
50
50
  },
51
51
  "devDependencies": {
52
52
  "clean-package": "^2.2.0",
@@ -15,7 +15,6 @@ import { BeanBase } from 'zova';
15
15
  import { TableCell } from 'zova-module-a-table';
16
16
 
17
17
  import { filterDetailsRowActions } from '../lib/detailsPermissions.js';
18
- import { checkFormScene } from '../lib/utils.js';
19
18
 
20
19
  declare module 'zova-module-a-openapi' {
21
20
  export interface IResourceDetailsActionRowRecord {
@@ -39,11 +38,13 @@ export class TableCellActionOperationsRow extends BeanBase implements ITableCell
39
38
  const actions = options.actions;
40
39
  if (!actions || actions.length === 0) return false;
41
40
  const $$details = $celScope.$$details!;
42
- const actionsVisible = actions.filter(action => {
43
- return checkFormScene($$details.formScene, action.options?.permission);
44
- });
41
+ const actionsAllowed = filterDetailsRowActions(
42
+ $$details.formScene,
43
+ $$details.checkPermission.bind($$details),
44
+ actions,
45
+ );
45
46
  const renders: TypeTableCellRenderComponent[] = [];
46
- for (const action of actionsVisible) {
47
+ for (const action of actionsAllowed) {
47
48
  const actionName = action.name;
48
49
  const actionRender = action.render;
49
50
  if (!actionRender) throw new Error(`should specify action render: ${actionName}`);
@@ -53,6 +53,7 @@ export class TableCellActionUpdate extends BeanBase implements ITableCellRender
53
53
  icon: options.dialogOptions?.icon,
54
54
  title: options.dialogOptions?.title ?? this.scope.locale.EditDetail(),
55
55
  dialogOptions: options.dialogOptions,
56
+ checkPermission: $$details.checkPermission.bind($$details),
56
57
  onSubmitData: (
57
58
  data: TypeFormOnSubmitData<Record<string, any>>,
58
59
  dialog: AppModalItem,
@@ -53,6 +53,7 @@ export class TableCellActionView extends BeanBase implements ITableCellRender {
53
53
  icon: options.dialogOptions?.icon,
54
54
  title: options.dialogOptions?.title ?? this.scope.locale.ViewDetail(),
55
55
  dialogOptions: options.dialogOptions,
56
+ checkPermission: $$details.checkPermission.bind($$details),
56
57
  } satisfies IDialogFormOptions);
57
58
  serverDetail.openDialogForm();
58
59
  }}
@@ -47,6 +47,7 @@ export class ControllerActionCreate extends BeanControllerBase {
47
47
  icon: this.$props.dialogOptions?.icon,
48
48
  title: this.$props.dialogOptions?.title ?? this.scope.locale.AddDetail(),
49
49
  dialogOptions: this.$props.dialogOptions,
50
+ checkPermission: $$details.checkPermission.bind($$details),
50
51
  onSubmitData: (
51
52
  data: TypeFormOnSubmitData<Record<string, any>>,
52
53
  dialog: AppModalItem,
@@ -81,14 +81,26 @@ export class ControllerFormFieldDetails extends BeanControllerBase {
81
81
  // blocks
82
82
  const blocks = schemaRow?.rest?.blocks;
83
83
  if (!blocks || blocks.length === 0) return;
84
- const checkPermission: TypeDetailsCheckPermission = createDetailsPermissionChecker(
85
- () =>
86
- (formFieldRenderContext.celScope as { permissions?: TypeOpenapiPermissions }).permissions,
87
- () => $$formField.$$form.formState.values as Record<string, unknown>,
88
- (permissions, actionName, permissionHint, currentData) => {
89
- return this.$passport.checkPermission(permissions, actionName, permissionHint, currentData);
90
- },
91
- );
84
+ const inheritedCheckPermission = (
85
+ formFieldRenderContext.celScope as {
86
+ checkPermission?: TypeDetailsCheckPermission;
87
+ }
88
+ ).checkPermission;
89
+ const checkPermission: TypeDetailsCheckPermission =
90
+ inheritedCheckPermission ??
91
+ createDetailsPermissionChecker(
92
+ () =>
93
+ (formFieldRenderContext.celScope as { permissions?: TypeOpenapiPermissions }).permissions,
94
+ () => $$formField.$$form.formState.values as Record<string, unknown>,
95
+ (permissions, actionName, permissionHint, currentData) => {
96
+ return this.$passport.checkPermission(
97
+ permissions,
98
+ actionName,
99
+ permissionHint,
100
+ currentData,
101
+ );
102
+ },
103
+ );
92
104
  const domBlocks: VNode[] = [];
93
105
  blocks.forEach((block, index) => {
94
106
  const options = deepExtend(
@@ -1,5 +1,5 @@
1
1
  import type {
2
- IPermissionHintGeneral,
2
+ IPermissionHintDetailsAction,
3
3
  IResourceRenderDetailsActionBulkOptionsAction,
4
4
  IResourceRenderDetailsActionRowOptionsAction,
5
5
  TypeFormScene,
@@ -12,7 +12,7 @@ import { checkFormScene } from './utils.js';
12
12
 
13
13
  export interface IDetailsPermissionAction {
14
14
  name?: string;
15
- options?: { permission?: IPermissionHintGeneral };
15
+ options?: { permission?: IPermissionHintDetailsAction };
16
16
  }
17
17
 
18
18
  export function createDetailsPermissionChecker(
@@ -21,7 +21,7 @@ export function createDetailsPermissionChecker(
21
21
  checkPermission: (
22
22
  permissions: TypeOpenapiPermissions | undefined,
23
23
  actionName?: string,
24
- permissionHint?: IPermissionHintGeneral,
24
+ permissionHint?: IPermissionHintDetailsAction,
25
25
  currentData?: Record<string, unknown>,
26
26
  ) => boolean,
27
27
  ): TypeDetailsCheckPermission {
@@ -36,7 +36,9 @@ export function isDetailsActionAllowed(
36
36
  action: IDetailsPermissionAction,
37
37
  ): boolean {
38
38
  const permissionHint = action.options?.permission;
39
- return checkFormScene(formScene, permissionHint) && checkPermission(action.name, permissionHint);
39
+ if (!checkFormScene(formScene, permissionHint)) return false;
40
+ if (permissionHint?.mode === 'formScene' && !permissionHint.actionInherit) return true;
41
+ return checkPermission(action.name, permissionHint);
40
42
  }
41
43
 
42
44
  export function filterDetailsActions<TAction extends IDetailsPermissionAction>(
@@ -91,6 +91,7 @@ export class ServiceDetail<TData extends {} = {}> extends BeanBase {
91
91
  return {
92
92
  formMeta: this.formMeta,
93
93
  $$detail,
94
+ checkPermission: this.options.checkPermission,
94
95
  };
95
96
  }
96
97
 
@@ -3,6 +3,7 @@ import 'zova-module-a-form';
3
3
  import 'zova-module-a-openapi';
4
4
 
5
5
  import type { ServiceDetail } from '../service/detail.jsx';
6
+ import type { TypeDetailsCheckPermission } from './details.js';
6
7
 
7
8
  declare module 'zova-module-a-openapi' {
8
9
  export interface IJsxRenderContextDetail<TData extends {} = {}> {
@@ -11,6 +12,7 @@ declare module 'zova-module-a-openapi' {
11
12
 
12
13
  export interface IDetailScope {
13
14
  $$detail?: ServiceDetail;
15
+ checkPermission?: TypeDetailsCheckPermission;
14
16
  }
15
17
  }
16
18
 
@@ -1,12 +1,12 @@
1
1
  import 'zova-module-a-table';
2
2
  import 'zova-module-a-openapi';
3
- import type { IDetailsScope, IPermissionHintGeneral } from 'zova-module-a-openapi';
3
+ import type { IDetailsScope, IPermissionHintDetailsAction } from 'zova-module-a-openapi';
4
4
 
5
5
  import { ControllerBlockDetails } from '../component/blockDetails/controller.jsx';
6
6
 
7
7
  export type TypeDetailsCheckPermission = (
8
8
  actionName?: string,
9
- permissionHint?: IPermissionHintGeneral,
9
+ permissionHint?: IPermissionHintDetailsAction,
10
10
  ) => boolean;
11
11
 
12
12
  declare module 'zova-module-a-openapi' {
@@ -3,6 +3,8 @@ import { IIconRecord } from 'zova-module-a-icon';
3
3
  import { TypeFormScene, TypeFormSchemaScene } from 'zova-module-a-openapi';
4
4
  import { AppModalItem, IModalDialogOptions } from 'zova-module-basic-app';
5
5
 
6
+ import type { TypeDetailsCheckPermission } from './details.js';
7
+
6
8
  export interface IDialogFormOptions<TData extends {} = {}> {
7
9
  locale: {
8
10
  Cancel: () => string;
@@ -15,6 +17,7 @@ export interface IDialogFormOptions<TData extends {} = {}> {
15
17
  dialogOptions?: IModalDialogOptions;
16
18
  icon?: keyof IIconRecord;
17
19
  title: string;
20
+ checkPermission?: TypeDetailsCheckPermission;
18
21
  onSubmitData?: (
19
22
  data: TypeFormOnSubmitData<TData>,
20
23
  dialogInstance: AppModalItem,
@@ -1,4 +1,8 @@
1
- import type { IPermissionHintGeneral, TypeOpenapiPermissions } from 'zova-module-a-openapi';
1
+ import type {
2
+ IPermissionHintDetailsAction,
3
+ IPermissionHintGeneral,
4
+ TypeOpenapiPermissions,
5
+ } from 'zova-module-a-openapi';
2
6
 
3
7
  import assert from 'node:assert/strict';
4
8
  import test from 'node:test';
@@ -123,3 +127,194 @@ test('row details actions use the same enclosing Resource checker and form-scene
123
127
  );
124
128
  assert.deepEqual(calls, ['allowed', 'denied']);
125
129
  });
130
+
131
+ test('form-scene CRUD actions do not invoke the enclosing permission checker', () => {
132
+ const calls: string[] = [];
133
+ const actions = ['create', 'update', 'delete'].map(name => ({
134
+ name,
135
+ options: { permission: { mode: 'formScene', formScene: ['create', 'edit'] } },
136
+ }));
137
+ const allowed = filterDetailsRowActions(
138
+ 'edit',
139
+ actionName => {
140
+ calls.push(actionName!);
141
+ return false;
142
+ },
143
+ actions as any,
144
+ );
145
+
146
+ assert.deepEqual(
147
+ allowed.map(action => action.name),
148
+ ['create', 'update', 'delete'],
149
+ );
150
+ assert.deepEqual(calls, []);
151
+ });
152
+
153
+ test('form-scene CRUD actions are filtered before permission checks in view', () => {
154
+ const calls: string[] = [];
155
+ const action = {
156
+ name: 'update',
157
+ options: { permission: { mode: 'formScene', formScene: ['create', 'edit'] } },
158
+ };
159
+
160
+ const allowed = filterDetailsRowActions(
161
+ 'view',
162
+ actionName => {
163
+ calls.push(actionName!);
164
+ return true;
165
+ },
166
+ [action] as any,
167
+ );
168
+
169
+ assert.deepEqual(allowed, []);
170
+ assert.deepEqual(calls, []);
171
+ });
172
+
173
+ test('action mode uses the literal action name and inherited hint', () => {
174
+ const calls: Array<[string | undefined, IPermissionHintDetailsAction | undefined]> = [];
175
+ const permissionHint = {
176
+ mode: 'action',
177
+ actionInherit: 'approve',
178
+ formScene: ['create', 'edit'],
179
+ };
180
+ const allowed = filterDetailsRowActions(
181
+ 'edit',
182
+ (actionName, hint) => {
183
+ calls.push([actionName, hint]);
184
+ return true;
185
+ },
186
+ [{ name: 'approve', options: { permission: permissionHint } }] as any,
187
+ );
188
+
189
+ assert.deepEqual(
190
+ allowed.map(action => action.name),
191
+ ['approve'],
192
+ );
193
+ assert.deepEqual(calls, [['approve', permissionHint]]);
194
+ });
195
+
196
+ test('omitted mode checks custom actions without inferring CRUD permissions', () => {
197
+ const calls: string[] = [];
198
+ const actions = ['create', 'update', 'delete'].map(name => ({ name }));
199
+ const allowed = filterDetailsRowActions(
200
+ 'create',
201
+ actionName => {
202
+ calls.push(actionName!);
203
+ return actionName === 'update';
204
+ },
205
+ actions as any,
206
+ );
207
+
208
+ assert.deepEqual(
209
+ allowed.map(action => action.name),
210
+ ['update'],
211
+ );
212
+ assert.deepEqual(calls, ['create', 'update', 'delete']);
213
+ });
214
+
215
+ test('form-scene mode with action inheritance still invokes the checker', () => {
216
+ const calls: Array<[string | undefined, IPermissionHintDetailsAction | undefined]> = [];
217
+ const permissionHint = {
218
+ mode: 'formScene',
219
+ actionInherit: 'grant',
220
+ formScene: ['create', 'edit'],
221
+ };
222
+ const allowed = filterDetailsRowActions(
223
+ 'create',
224
+ (actionName, hint) => {
225
+ calls.push([actionName, hint]);
226
+ return actionName === 'update';
227
+ },
228
+ [{ name: 'update', options: { permission: permissionHint } }] as any,
229
+ );
230
+
231
+ assert.deepEqual(
232
+ allowed.map(action => action.name),
233
+ ['update'],
234
+ );
235
+ assert.deepEqual(calls, [['update', permissionHint]]);
236
+ });
237
+
238
+ test('view scene hides nested write actions before permission checks', () => {
239
+ const calls: string[] = [];
240
+ const actions = [
241
+ {
242
+ name: 'create',
243
+ options: { permission: { mode: 'formScene', formScene: ['create', 'edit'] } },
244
+ },
245
+ {
246
+ name: 'update',
247
+ options: { permission: { mode: 'formScene', formScene: ['create', 'edit'] } },
248
+ },
249
+ {
250
+ name: 'delete',
251
+ options: { permission: { mode: 'formScene', formScene: ['create', 'edit'] } },
252
+ },
253
+ { name: 'view', options: { permission: { actionInherit: 'view' } } },
254
+ ];
255
+
256
+ const allowed = filterDetailsRowActions(
257
+ 'view',
258
+ actionName => {
259
+ calls.push(actionName!);
260
+ return actionName === 'view';
261
+ },
262
+ actions as any,
263
+ );
264
+
265
+ assert.deepEqual(
266
+ allowed.map(action => action.name),
267
+ ['view'],
268
+ );
269
+ assert.deepEqual(calls, ['view']);
270
+ });
271
+
272
+ test('view scene preserves explicit inherited write actions', () => {
273
+ const calls: string[] = [];
274
+ const actions = [
275
+ { name: 'update', options: { permission: { actionInherit: 'grant', formScene: ['view'] } } },
276
+ ];
277
+
278
+ const allowed = filterDetailsRowActions(
279
+ 'view',
280
+ (actionName, permissionHint) => {
281
+ calls.push(actionName!);
282
+ return actionName === 'update' && permissionHint?.actionInherit === 'grant';
283
+ },
284
+ actions as any,
285
+ );
286
+
287
+ assert.deepEqual(
288
+ allowed.map(action => action.name),
289
+ ['update'],
290
+ );
291
+ assert.deepEqual(calls, ['update']);
292
+ });
293
+
294
+ test('explicit action inheritance and custom actions are preserved', () => {
295
+ const calls: Array<[string | undefined, IPermissionHintGeneral | undefined]> = [];
296
+ const explicitHint = { actionInherit: 'grant', formScene: ['create', 'edit'] };
297
+ const customHint = { mode: 'action', formScene: ['create', 'edit'] };
298
+ const actions = [
299
+ { name: 'update', options: { permission: explicitHint } },
300
+ { name: 'archive', options: { permission: customHint } },
301
+ ];
302
+
303
+ const allowed = filterDetailsRowActions(
304
+ 'create',
305
+ (actionName, permissionHint) => {
306
+ calls.push([actionName, permissionHint]);
307
+ return true;
308
+ },
309
+ actions as any,
310
+ );
311
+
312
+ assert.deepEqual(
313
+ allowed.map(action => action.name),
314
+ ['update', 'archive'],
315
+ );
316
+ assert.deepEqual(calls, [
317
+ ['update', explicitHint],
318
+ ['archive', customHint],
319
+ ]);
320
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-openapi",
3
- "version": "5.1.56",
3
+ "version": "5.1.57",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "",
6
6
  "keywords": [
@@ -52,6 +52,12 @@ export interface IPermissionHintTableActionBulk {
52
52
  public?: boolean;
53
53
  }
54
54
 
55
- export interface IPermissionHintDetailsActionRow extends IPermissionHintGeneral {}
55
+ export type TypeDetailsPermissionMode = 'formScene' | 'action';
56
56
 
57
- export interface IPermissionHintDetailsActionBulk extends IPermissionHintGeneral {}
57
+ export interface IPermissionHintDetailsAction extends IPermissionHintGeneral {
58
+ mode?: TypeDetailsPermissionMode;
59
+ }
60
+
61
+ export interface IPermissionHintDetailsActionRow extends IPermissionHintDetailsAction {}
62
+
63
+ export interface IPermissionHintDetailsActionBulk extends IPermissionHintDetailsAction {}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-suite-a-zova",
3
- "version": "5.1.169",
3
+ "version": "5.1.170",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "zova",
6
6
  "license": "MIT",
@@ -21,7 +21,7 @@
21
21
  "zova-module-a-logger": "^5.1.28",
22
22
  "zova-module-a-meta": "^5.1.22",
23
23
  "zova-module-a-model": "^5.1.38",
24
- "zova-module-a-openapi": "^5.1.56",
24
+ "zova-module-a-openapi": "^5.1.57",
25
25
  "zova-module-a-router": "^5.1.42",
26
26
  "zova-module-a-routerstack": "^5.1.29",
27
27
  "zova-module-a-routertabs": "^5.1.35",