diff --git a/src-ui/src/app/components/common/permissions-select/permissions-select.component.html b/src-ui/src/app/components/common/permissions-select/permissions-select.component.html index f176140cd..19382225a 100644 --- a/src-ui/src/app/components/common/permissions-select/permissions-select.component.html +++ b/src-ui/src/app/components/common/permissions-select/permissions-select.component.html @@ -26,8 +26,8 @@ - @for (action of PermissionAction | keyvalue; track action) { -
+ @for (action of PermissionAction | keyvalue: sortActions; track action.key) { +
diff --git a/src-ui/src/app/components/common/permissions-select/permissions-select.component.spec.ts b/src-ui/src/app/components/common/permissions-select/permissions-select.component.spec.ts index 0aebf1ee3..c84e9cc3f 100644 --- a/src-ui/src/app/components/common/permissions-select/permissions-select.component.spec.ts +++ b/src-ui/src/app/components/common/permissions-select/permissions-select.component.spec.ts @@ -75,7 +75,6 @@ describe('PermissionsSelectComponent', () => { it('should update on permissions set', () => { component.ngOnInit() component.writeValue(permissions) - expect(permissionsChangeResult).toEqual(permissions) expect(component.typesWithAllActions).toContain('Document') }) @@ -92,13 +91,12 @@ describe('PermissionsSelectComponent', () => { it('disable checkboxes when permissions are inherited', () => { component.ngOnInit() component.inheritedPermissions = inheritedPermissions + fixture.detectChanges() expect(component.isInherited('Document', 'Add')).toBeFalsy() expect(component.isInherited('Document')).toBeFalsy() expect(component.isInherited('Tag', 'Change')).toBeTruthy() - const input1 = fixture.debugElement.query(By.css('input#Document_Add')) - expect(input1.nativeElement.disabled).toBeFalsy() - const input2 = fixture.debugElement.query(By.css('input#Tag_Change')) - expect(input2.nativeElement.disabled).toBeTruthy() + expect(component.form.get('Document').get('Add').disabled).toBeFalsy() + expect(component.form.get('Tag').get('Change').disabled).toBeTruthy() }) it('should exclude history permissions if disabled', () => { @@ -107,4 +105,32 @@ describe('PermissionsSelectComponent', () => { component = fixture.componentInstance expect(component.allowedTypes).not.toContain('History') }) + + it('should treat global statistics as view-only', () => { + component.ngOnInit() + fixture.detectChanges() + + expect( + component.isActionSupported( + PermissionType.GlobalStatistics, + PermissionAction.View + ) + ).toBeTruthy() + expect( + component.isActionSupported( + PermissionType.GlobalStatistics, + PermissionAction.Add + ) + ).toBeFalsy() + + const addInput = fixture.debugElement.query( + By.css('input#GlobalStatistics_Add') + ) + const viewInput = fixture.debugElement.query( + By.css('input#GlobalStatistics_View') + ) + + expect(addInput.nativeElement.disabled).toBeTruthy() + expect(viewInput.nativeElement.disabled).toBeFalsy() + }) }) diff --git a/src-ui/src/app/components/common/permissions-select/permissions-select.component.ts b/src-ui/src/app/components/common/permissions-select/permissions-select.component.ts index 689fefa17..ebb04aaef 100644 --- a/src-ui/src/app/components/common/permissions-select/permissions-select.component.ts +++ b/src-ui/src/app/components/common/permissions-select/permissions-select.component.ts @@ -1,4 +1,4 @@ -import { KeyValuePipe } from '@angular/common' +import { KeyValue, KeyValuePipe } from '@angular/common' import { Component, forwardRef, inject, Input, OnInit } from '@angular/core' import { AbstractControl, @@ -58,6 +58,13 @@ export class PermissionsSelectComponent typesWithAllActions: Set = new Set() + private readonly actionOrder = [ + PermissionAction.Add, + PermissionAction.Change, + PermissionAction.Delete, + PermissionAction.View, + ] + _inheritedPermissions: string[] = [] @Input() @@ -86,7 +93,7 @@ export class PermissionsSelectComponent } this.allowedTypes.forEach((type) => { const control = new FormGroup({}) - for (const action in PermissionAction) { + for (const action of Object.keys(PermissionAction)) { control.addControl(action, new FormControl(null)) } this.form.addControl(type, control) @@ -106,18 +113,14 @@ export class PermissionsSelectComponent this.permissionsService.getPermissionKeys(permissionStr) if (actionKey && typeKey) { - if (this.form.get(typeKey)?.get(actionKey)) { - this.form - .get(typeKey) - .get(actionKey) - .patchValue(true, { emitEvent: false }) - } + this.form + .get(typeKey) + ?.get(actionKey) + ?.patchValue(true, { emitEvent: false }) } }) this.allowedTypes.forEach((type) => { - if ( - Object.values(this.form.get(type).value).every((val) => val == true) - ) { + if (this.typeHasAllActionsSelected(type)) { this.typesWithAllActions.add(type) } else { this.typesWithAllActions.delete(type) @@ -149,12 +152,15 @@ export class PermissionsSelectComponent this.form.valueChanges.subscribe((newValue) => { let permissions = [] Object.entries(newValue).forEach(([typeKey, typeValue]) => { - // e.g. [Document, { Add: true, View: true ... }] const selectedActions = Object.entries(typeValue).filter( - ([actionKey, actionValue]) => actionValue == true + ([actionKey, actionValue]) => + this.isActionSupported( + PermissionType[typeKey], + PermissionAction[actionKey] + ) && actionValue == true ) - selectedActions.forEach(([actionKey, actionValue]) => { + selectedActions.forEach(([actionKey]) => { permissions.push( (PermissionType[typeKey] as string).replace( '%s', @@ -163,7 +169,7 @@ export class PermissionsSelectComponent ) }) - if (selectedActions.length == Object.entries(typeValue).length) { + if (this.typeHasAllActionsSelected(typeKey)) { this.typesWithAllActions.add(typeKey) } else { this.typesWithAllActions.delete(typeKey) @@ -174,19 +180,23 @@ export class PermissionsSelectComponent permissions.filter((p) => !this._inheritedPermissions.includes(p)) ) }) + + this.updateDisabledStates() } toggleAll(event, type) { const typeGroup = this.form.get(type) - if (event.target.checked) { - Object.keys(PermissionAction).forEach((action) => { - typeGroup.get(action).patchValue(true) + Object.keys(PermissionAction) + .filter((action) => + this.isActionSupported(PermissionType[type], PermissionAction[action]) + ) + .forEach((action) => { + typeGroup.get(action).patchValue(event.target.checked) }) + + if (this.typeHasAllActionsSelected(type)) { this.typesWithAllActions.add(type) } else { - Object.keys(PermissionAction).forEach((action) => { - typeGroup.get(action).patchValue(false) - }) this.typesWithAllActions.delete(type) } } @@ -201,14 +211,21 @@ export class PermissionsSelectComponent ) ) } else { - return Object.values(PermissionAction).every((action) => { - return this._inheritedPermissions.includes( - this.permissionsService.getPermissionCode( - action as PermissionAction, - PermissionType[typeKey] + return Object.keys(PermissionAction) + .filter((action) => + this.isActionSupported( + PermissionType[typeKey], + PermissionAction[action] ) ) - }) + .every((action) => { + return this._inheritedPermissions.includes( + this.permissionsService.getPermissionCode( + PermissionAction[action], + PermissionType[typeKey] + ) + ) + }) } } @@ -216,12 +233,49 @@ export class PermissionsSelectComponent this.allowedTypes.forEach((type) => { const control = this.form.get(type) let actionControl: AbstractControl - for (const action in PermissionAction) { + for (const action of Object.keys(PermissionAction)) { actionControl = control.get(action) + if ( + !this.isActionSupported( + PermissionType[type], + PermissionAction[action] + ) + ) { + actionControl.patchValue(false, { emitEvent: false }) + actionControl.disable({ emitEvent: false }) + continue + } + this.isInherited(type, action) || this.disabled - ? actionControl.disable() - : actionControl.enable() + ? actionControl.disable({ emitEvent: false }) + : actionControl.enable({ emitEvent: false }) } }) } + + isActionSupported(type: PermissionType, action: PermissionAction): boolean { + // Global statistics only supports view permissions + if (type === PermissionType.GlobalStatistics) { + return action === PermissionAction.View + } + + return true + } + + private typeHasAllActionsSelected(typeKey: string): boolean { + return Object.keys(PermissionAction) + .filter((action) => + this.isActionSupported( + PermissionType[typeKey], + PermissionAction[action] + ) + ) + .every((action) => this.form.get(typeKey)?.get(action)?.value == true) + } + + sortActions = ( + a: KeyValue, + b: KeyValue + ): number => + this.actionOrder.indexOf(a.value) - this.actionOrder.indexOf(b.value) } diff --git a/src-ui/src/app/services/permissions.service.ts b/src-ui/src/app/services/permissions.service.ts index 0c36b646f..8fd59a8ca 100644 --- a/src-ui/src/app/services/permissions.service.ts +++ b/src-ui/src/app/services/permissions.service.ts @@ -29,6 +29,7 @@ export enum PermissionType { CustomField = '%s_customfield', Workflow = '%s_workflow', ProcessedMail = '%s_processedmail', + GlobalStatistics = '%s_global_statistics', } @Injectable({