cabloy 5.1.212 → 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 (24) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/package.json +1 -1
  4. package/repo-docs/frontend/permission-formscene-action-visibility-guide.md +7 -5
  5. package/vona/packages-cli/cli/package.json +1 -1
  6. package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/<%=argv.detailDtoResItemName%>.tsx_ +3 -3
  7. package/vona/packages-cli/cli-set-api/package.json +1 -1
  8. package/vona/src/suite/a-training/modules/training-record/src/dto/detailRecordSubjectResItem.tsx +3 -3
  9. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordResItem.tsx +3 -3
  10. package/zova/packages-zova/zova/package.json +2 -2
  11. package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionOperationsRow.tsx +6 -5
  12. package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionUpdate.tsx +1 -0
  13. package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionView.tsx +1 -0
  14. package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/actionCreate/controller.tsx +1 -0
  15. package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/formFieldDetails/controller.tsx +20 -8
  16. package/zova/src/suite/cabloy-basic/modules/basic-details/src/lib/detailsPermissions.ts +6 -4
  17. package/zova/src/suite/cabloy-basic/modules/basic-details/src/service/detail.tsx +1 -0
  18. package/zova/src/suite/cabloy-basic/modules/basic-details/src/types/detail.ts +2 -0
  19. package/zova/src/suite/cabloy-basic/modules/basic-details/src/types/details.ts +2 -2
  20. package/zova/src/suite/cabloy-basic/modules/basic-details/src/types/dialogForm.ts +3 -0
  21. package/zova/src/suite/cabloy-basic/modules/basic-details/test/lib/detailsPermissions.test.ts +196 -1
  22. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  23. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/permissions.ts +8 -2
  24. package/zova/src/suite-vendor/a-zova/package.json +2 -2
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.212
1
+ 5.1.213
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.213
4
+
5
+ ### Bug Fixes
6
+
7
+ - Fix details.
8
+
3
9
  ## 5.1.212
4
10
 
5
11
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.212",
3
+ "version": "5.1.213",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -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",