Frontend support for merge flag with bulk edit permissions

This commit is contained in:
shamoon 2024-01-22 13:44:42 -08:00
parent 93f3c3aa7b
commit 4338493e46
13 changed files with 146 additions and 56 deletions

View File

@ -620,7 +620,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">23</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context> <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
@ -2438,7 +2438,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
<context context-type="linenumber">23</context> <context context-type="linenumber">26</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@ -2505,7 +2505,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
<context context-type="linenumber">22</context> <context context-type="linenumber">25</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@ -3923,6 +3923,13 @@
<context context-type="linenumber">15</context> <context context-type="linenumber">15</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7940755769131903278" datatype="html">
<source>Merge with existing permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
<context context-type="linenumber">14</context>
</context-group>
</trans-unit>
<trans-unit id="7062872617520618723" datatype="html"> <trans-unit id="7062872617520618723" datatype="html">
<source>Set permissions</source> <source>Set permissions</source>
<context-group purpose="location"> <context-group purpose="location">
@ -3937,11 +3944,18 @@
<context context-type="linenumber">33</context> <context context-type="linenumber">33</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8283439432608484491" datatype="html"> <trans-unit id="347498040201588614" datatype="html">
<source>Note that permissions set here will override any existing permissions</source> <source>Existing owner, user and group permissions will be merged with these settings.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.ts</context>
<context context-type="linenumber">71</context> <context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit id="3434726483516379481" datatype="html">
<source>Any and all existing owner, user and group permissions will be replaced.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.ts</context>
<context context-type="linenumber">75</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5947558132119506443" datatype="html"> <trans-unit id="5947558132119506443" datatype="html">
@ -6100,18 +6114,18 @@
<source>Permissions updated</source> <source>Permissions updated</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context>
<context context-type="linenumber">211</context> <context context-type="linenumber">212</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4639647950943944112" datatype="html"> <trans-unit id="4639647950943944112" datatype="html">
<source>Error updating permissions</source> <source>Error updating permissions</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context>
<context context-type="linenumber">215</context> <context context-type="linenumber">217</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">300</context> <context context-type="linenumber">301</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4010735610815226758" datatype="html"> <trans-unit id="4010735610815226758" datatype="html">
@ -6277,7 +6291,7 @@
<source>Permissions updated successfully</source> <source>Permissions updated successfully</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">293</context> <context context-type="linenumber">294</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5101757640976222639" datatype="html"> <trans-unit id="5101757640976222639" datatype="html">

View File

@ -15,7 +15,7 @@
} }
</div> </div>
} }
<div [ngClass]="{'col-md-9': horizontal, 'align-items-center': horizontal, 'd-flex': horizontal}"> <div [ngClass]="{'align-items-center': horizontal, 'd-flex': horizontal}">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled"> <input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
@if (horizontal) { @if (horizontal) {

View File

@ -5,12 +5,15 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
@if (!object && message) {
<p class="mb-3" [innerHTML]="message | safeHtml"></p>
}
<form [formGroup]="form"> <form [formGroup]="form">
<pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form> <div class="form-group">
<pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form>
</div>
<div class="form-group mt-4">
<div class="offset-lg-3 row">
<pngx-input-switch i18n-title title="Merge with existing permissions" [horizontal]="true" [hint]="hint" formControlName="merge"></pngx-input-switch>
</div>
</div>
</form> </form>
</div> </div>
@ -20,5 +23,5 @@
<span class="visually-hidden" i18n>Loading...</span> <span class="visually-hidden" i18n>Loading...</span>
} }
<button type="button" class="btn btn-outline-primary" (click)="cancelClicked()" [disabled]="!buttonsEnabled" i18n>Cancel</button> <button type="button" class="btn btn-outline-primary" (click)="cancelClicked()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
<button type="button" class="btn btn-primary" (click)="confirmClicked.emit(permissions)" [disabled]="!buttonsEnabled" i18n>Confirm</button> <button type="button" class="btn btn-primary" (click)="confirm()" [disabled]="!buttonsEnabled" i18n>Confirm</button>
</div> </div>

View File

@ -11,6 +11,7 @@ import { NgSelectModule } from '@ng-select/ng-select'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { PermissionsUserComponent } from '../input/permissions/permissions-user/permissions-user.component' import { PermissionsUserComponent } from '../input/permissions/permissions-user/permissions-user.component'
import { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component' import { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component'
import { SwitchComponent } from '../input/switch/switch.component'
const set_permissions = { const set_permissions = {
owner: 10, owner: 10,
@ -37,6 +38,7 @@ describe('PermissionsDialogComponent', () => {
PermissionsDialogComponent, PermissionsDialogComponent,
SafeHtmlPipe, SafeHtmlPipe,
SelectComponent, SelectComponent,
SwitchComponent,
PermissionsFormComponent, PermissionsFormComponent,
PermissionsUserComponent, PermissionsUserComponent,
PermissionsGroupComponent, PermissionsGroupComponent,
@ -112,4 +114,23 @@ describe('PermissionsDialogComponent', () => {
expect(component.title).toEqual(`Edit permissions for ${obj.name}`) expect(component.title).toEqual(`Edit permissions for ${obj.name}`)
expect(component.permissions).toEqual(set_permissions) expect(component.permissions).toEqual(set_permissions)
}) })
it('should toggle hint based on object existence (if editing) or merge flag', () => {
component.form.get('merge').setValue(true)
expect(component.hint.includes('Existing')).toBeTruthy()
component.form.get('merge').setValue(false)
expect(component.hint.includes('will be replaced')).toBeTruthy()
component.object = {}
expect(component.hint).toBeNull()
})
it('should emit permissions and merge flag on confirm', () => {
const confirmSpy = jest.spyOn(component.confirmClicked, 'emit')
component.form.get('permissions_form').setValue(set_permissions)
component.confirm()
expect(confirmSpy).toHaveBeenCalledWith({
permissions: set_permissions,
merge: true,
})
})
}) })

View File

@ -32,6 +32,7 @@ export class PermissionsDialogComponent {
this.o = o this.o = o
this.title = $localize`Edit permissions for ` + o['name'] this.title = $localize`Edit permissions for ` + o['name']
this.form.patchValue({ this.form.patchValue({
merge: true,
permissions_form: { permissions_form: {
owner: o.owner, owner: o.owner,
set_permissions: o.permissions, set_permissions: o.permissions,
@ -43,8 +44,9 @@ export class PermissionsDialogComponent {
return this.o return this.o
} }
form = new FormGroup({ public form = new FormGroup({
permissions_form: new FormControl(), permissions_form: new FormControl(),
merge: new FormControl(true),
}) })
buttonsEnabled: boolean = true buttonsEnabled: boolean = true
@ -66,11 +68,21 @@ export class PermissionsDialogComponent {
} }
} }
@Input() get hint(): string {
message = if (this.object) return null
$localize`Note that permissions set here will override any existing permissions` return this.form.get('merge').value
? $localize`Existing owner, user and group permissions will be merged with these settings.`
: $localize`Any and all existing owner, user and group permissions will be replaced.`
}
cancelClicked() { cancelClicked() {
this.activeModal.close() this.activeModal.close()
} }
confirm() {
this.confirmClicked.emit({
permissions: this.permissions,
merge: this.form.get('merge').value,
})
}
} }

View File

@ -41,6 +41,7 @@ import { PermissionsUserComponent } from '../../common/input/permissions/permiss
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { GroupService } from 'src/app/services/rest/group.service' import { GroupService } from 'src/app/services/rest/group.service'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SwitchComponent } from '../../common/input/switch/switch.component'
const selectionData: SelectionData = { const selectionData: SelectionData = {
selected_tags: [ selected_tags: [
@ -81,6 +82,7 @@ describe('BulkEditorComponent', () => {
SelectComponent, SelectComponent,
PermissionsGroupComponent, PermissionsGroupComponent,
PermissionsUserComponent, PermissionsUserComponent,
SwitchComponent,
], ],
providers: [ providers: [
PermissionsService, PermissionsService,
@ -851,7 +853,18 @@ describe('BulkEditorComponent', () => {
fixture.detectChanges() fixture.detectChanges()
component.setPermissions() component.setPermissions()
expect(modal).not.toBeUndefined() expect(modal).not.toBeUndefined()
modal.componentInstance.confirmClicked.next() const perms = {
permissions: {
view_users: [],
change_users: [],
view_groups: [],
change_groups: [],
},
}
modal.componentInstance.confirmClicked.emit({
permissions: perms,
merge: true,
})
let req = httpTestingController.expectOne( let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/bulk_edit/` `${environment.apiBaseUrl}documents/bulk_edit/`
) )
@ -859,7 +872,10 @@ describe('BulkEditorComponent', () => {
expect(req.request.body).toEqual({ expect(req.request.body).toEqual({
documents: [3, 4], documents: [3, 4],
method: 'set_permissions', method: 'set_permissions',
parameters: undefined, parameters: {
permissions: perms.permissions,
merge: true,
},
}) })
httpTestingController.match( httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true` `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`

View File

@ -540,9 +540,14 @@ export class BulkEditorComponent
let modal = this.modalService.open(PermissionsDialogComponent, { let modal = this.modalService.open(PermissionsDialogComponent, {
backdrop: 'static', backdrop: 'static',
}) })
modal.componentInstance.confirmClicked.subscribe((permissions) => { modal.componentInstance.confirmClicked.subscribe(
modal.componentInstance.buttonsEnabled = false ({ permissions, merge }) => {
this.executeBulkOperation(modal, 'set_permissions', permissions) modal.componentInstance.buttonsEnabled = false
}) this.executeBulkOperation(modal, 'set_permissions', {
...permissions,
merge,
})
}
)
} }
} }

View File

@ -41,6 +41,7 @@ import { TagsComponent } from '../../common/input/tags/tags.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SwitchComponent } from '../../common/input/switch/switch.component'
const mailAccounts = [ const mailAccounts = [
{ id: 1, name: 'account1' }, { id: 1, name: 'account1' },
@ -82,6 +83,7 @@ describe('MailComponent', () => {
PermissionsGroupComponent, PermissionsGroupComponent,
PermissionsDialogComponent, PermissionsDialogComponent,
PermissionsFormComponent, PermissionsFormComponent,
SwitchComponent,
], ],
providers: [CustomDatePipe, DatePipe, PermissionsGuard], providers: [CustomDatePipe, DatePipe, PermissionsGuard],
imports: [ imports: [
@ -267,11 +269,11 @@ describe('MailComponent', () => {
rulePatchSpy.mockReturnValueOnce( rulePatchSpy.mockReturnValueOnce(
throwError(() => new Error('error saving perms')) throwError(() => new Error('error saving perms'))
) )
dialog.confirmClicked.emit(perms) dialog.confirmClicked.emit({ permissions: perms, merge: true })
expect(rulePatchSpy).toHaveBeenCalled() expect(rulePatchSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toHaveBeenCalled()
rulePatchSpy.mockReturnValueOnce(of(mailRules[0] as MailRule)) rulePatchSpy.mockReturnValueOnce(of(mailRules[0] as MailRule))
dialog.confirmClicked.emit(perms) dialog.confirmClicked.emit({ permissions: perms, merge: true })
expect(toastInfoSpy).toHaveBeenCalledWith('Permissions updated') expect(toastInfoSpy).toHaveBeenCalledWith('Permissions updated')
modalService.dismissAll() modalService.dismissAll()
@ -299,8 +301,7 @@ describe('MailComponent', () => {
expect(modal).not.toBeUndefined() expect(modal).not.toBeUndefined()
let dialog = modal.componentInstance as PermissionsDialogComponent let dialog = modal.componentInstance as PermissionsDialogComponent
expect(dialog.object).toEqual(mailAccounts[0]) expect(dialog.object).toEqual(mailAccounts[0])
dialog = modal.componentInstance as PermissionsDialogComponent dialog.confirmClicked.emit({ permissions: perms, merge: true })
dialog.confirmClicked.emit(perms)
expect(accountPatchSpy).toHaveBeenCalled() expect(accountPatchSpy).toHaveBeenCalled()
}) })
}) })

View File

@ -200,22 +200,27 @@ export class MailComponent
const dialog: PermissionsDialogComponent = const dialog: PermissionsDialogComponent =
modal.componentInstance as PermissionsDialogComponent modal.componentInstance as PermissionsDialogComponent
dialog.object = object dialog.object = object
modal.componentInstance.confirmClicked.subscribe((permissions) => { modal.componentInstance.confirmClicked.subscribe(
modal.componentInstance.buttonsEnabled = false ({ permissions, merge }) => {
const service: AbstractPaperlessService<MailRule | MailAccount> = modal.componentInstance.buttonsEnabled = false
'account' in object ? this.mailRuleService : this.mailAccountService const service: AbstractPaperlessService<MailRule | MailAccount> =
object.owner = permissions['owner'] 'account' in object ? this.mailRuleService : this.mailAccountService
object['set_permissions'] = permissions['set_permissions'] object.owner = permissions['owner']
service.patch(object).subscribe({ object['set_permissions'] = permissions['set_permissions']
next: () => { service.patch(object).subscribe({
this.toastService.showInfo($localize`Permissions updated`) next: () => {
modal.close() this.toastService.showInfo($localize`Permissions updated`)
}, modal.close()
error: (e) => { },
this.toastService.showError($localize`Error updating permissions`, e) error: (e) => {
}, this.toastService.showError(
}) $localize`Error updating permissions`,
}) e
)
},
})
}
)
} }
userCanEdit(obj: ObjectWithPermissions): boolean { userCanEdit(obj: ObjectWithPermissions): boolean {

View File

@ -264,13 +264,19 @@ describe('ManagementListComponent', () => {
throwError(() => new Error('error setting permissions')) throwError(() => new Error('error setting permissions'))
) )
const errorToastSpy = jest.spyOn(toastService, 'showError') const errorToastSpy = jest.spyOn(toastService, 'showError')
modal.componentInstance.confirmClicked.emit() modal.componentInstance.confirmClicked.emit({
permissions: {},
merge: true,
})
expect(bulkEditPermsSpy).toHaveBeenCalled() expect(bulkEditPermsSpy).toHaveBeenCalled()
expect(errorToastSpy).toHaveBeenCalled() expect(errorToastSpy).toHaveBeenCalled()
const successToastSpy = jest.spyOn(toastService, 'showInfo') const successToastSpy = jest.spyOn(toastService, 'showInfo')
bulkEditPermsSpy.mockReturnValueOnce(of('OK')) bulkEditPermsSpy.mockReturnValueOnce(of('OK'))
modal.componentInstance.confirmClicked.emit() modal.componentInstance.confirmClicked.emit({
permissions: {},
merge: true,
})
expect(bulkEditPermsSpy).toHaveBeenCalled() expect(bulkEditPermsSpy).toHaveBeenCalled()
expect(successToastSpy).toHaveBeenCalled() expect(successToastSpy).toHaveBeenCalled()
}) })

View File

@ -279,12 +279,13 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
backdrop: 'static', backdrop: 'static',
}) })
modal.componentInstance.confirmClicked.subscribe( modal.componentInstance.confirmClicked.subscribe(
(permissions: { owner: number; set_permissions: PermissionsObject }) => { ({ permissions, merge }) => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled = false
this.service this.service
.bulk_update_permissions( .bulk_update_permissions(
Array.from(this.selectedObjects), Array.from(this.selectedObjects),
permissions permissions,
merge
) )
.subscribe({ .subscribe({
next: () => { next: () => {

View File

@ -53,10 +53,14 @@ export const commonAbstractNameFilterPaperlessServiceTests = (
}, },
} }
subscription = service subscription = service
.bulk_update_permissions([1, 2], { .bulk_update_permissions(
owner, [1, 2],
set_permissions: permissions, {
}) owner,
set_permissions: permissions,
},
true
)
.subscribe() .subscribe()
const req = httpTestingController.expectOne( const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}bulk_edit_object_perms/` `${environment.apiBaseUrl}bulk_edit_object_perms/`

View File

@ -26,13 +26,15 @@ export abstract class AbstractNameFilterService<
bulk_update_permissions( bulk_update_permissions(
objects: Array<number>, objects: Array<number>,
permissions: { owner: number; set_permissions: PermissionsObject } permissions: { owner: number; set_permissions: PermissionsObject },
merge: boolean
): Observable<string> { ): Observable<string> {
return this.http.post<string>(`${this.baseUrl}bulk_edit_object_perms/`, { return this.http.post<string>(`${this.baseUrl}bulk_edit_object_perms/`, {
objects, objects,
object_type: this.resourceName, object_type: this.resourceName,
owner: permissions.owner, owner: permissions.owner,
permissions: permissions.set_permissions, permissions: permissions.set_permissions,
merge,
}) })
} }
} }