mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-28 03:46:06 -05:00 
			
		
		
		
	skeleton user / group admin dialogs [WIP]
This commit is contained in:
		| @@ -78,6 +78,9 @@ import { StoragePathEditDialogComponent } from './components/common/edit-dialog/ | |||||||
| import { SettingsService } from './services/settings.service' | import { SettingsService } from './services/settings.service' | ||||||
| import { TasksComponent } from './components/manage/tasks/tasks.component' | import { TasksComponent } from './components/manage/tasks/tasks.component' | ||||||
| import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap' | import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap' | ||||||
|  | import { UserEditDialogComponent } from './components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component' | ||||||
|  | import { GroupEditDialogComponent } from './components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component' | ||||||
|  | import { PermissionsSelectComponent } from './components/common/permissions-select/permissions-select.component' | ||||||
|  |  | ||||||
| import localeBe from '@angular/common/locales/be' | import localeBe from '@angular/common/locales/be' | ||||||
| import localeCs from '@angular/common/locales/cs' | import localeCs from '@angular/common/locales/cs' | ||||||
| @@ -183,6 +186,9 @@ function initializeApp(settings: SettingsService) { | |||||||
|     DocumentAsnComponent, |     DocumentAsnComponent, | ||||||
|     DocumentCommentsComponent, |     DocumentCommentsComponent, | ||||||
|     TasksComponent, |     TasksComponent, | ||||||
|  |     UserEditDialogComponent, | ||||||
|  |     GroupEditDialogComponent, | ||||||
|  |     PermissionsSelectComponent, | ||||||
|   ], |   ], | ||||||
|   imports: [ |   imports: [ | ||||||
|     BrowserModule, |     BrowserModule, | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit- | |||||||
| import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | ||||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' |  | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-correspondent-edit-dialog', |   selector: 'app-correspondent-edit-dialog', | ||||||
| @@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service' | |||||||
|   styleUrls: ['./correspondent-edit-dialog.component.scss'], |   styleUrls: ['./correspondent-edit-dialog.component.scss'], | ||||||
| }) | }) | ||||||
| export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> { | export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> { | ||||||
|   constructor( |   constructor(service: CorrespondentService, activeModal: NgbActiveModal) { | ||||||
|     service: CorrespondentService, |     super(service, activeModal) | ||||||
|     activeModal: NgbActiveModal, |  | ||||||
|     toastService: ToastService |  | ||||||
|   ) { |  | ||||||
|     super(service, activeModal, toastService) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getCreateTitle() { |   getCreateTitle() { | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit- | |||||||
| import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | ||||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' |  | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-document-type-edit-dialog', |   selector: 'app-document-type-edit-dialog', | ||||||
| @@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service' | |||||||
|   styleUrls: ['./document-type-edit-dialog.component.scss'], |   styleUrls: ['./document-type-edit-dialog.component.scss'], | ||||||
| }) | }) | ||||||
| export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> { | export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> { | ||||||
|   constructor( |   constructor(service: DocumentTypeService, activeModal: NgbActiveModal) { | ||||||
|     service: DocumentTypeService, |     super(service, activeModal) | ||||||
|     activeModal: NgbActiveModal, |  | ||||||
|     toastService: ToastService |  | ||||||
|   ) { |  | ||||||
|     super(service, activeModal, toastService) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getCreateTitle() { |   getCreateTitle() { | ||||||
|   | |||||||
| @@ -14,8 +14,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> | |||||||
| { | { | ||||||
|   constructor( |   constructor( | ||||||
|     private service: AbstractPaperlessService<T>, |     private service: AbstractPaperlessService<T>, | ||||||
|     private activeModal: NgbActiveModal, |     private activeModal: NgbActiveModal | ||||||
|     private toastService: ToastService |  | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   | |||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | <form [formGroup]="objectForm" (ngSubmit)="save()"> | ||||||
|  |     <div class="modal-header"> | ||||||
|  |       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||||
|  |       <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||||
|  |       </button> | ||||||
|  |     </div> | ||||||
|  |     <div class="modal-body"> | ||||||
|  |       <div class="row"> | ||||||
|  |         <div class="col"> | ||||||
|  |           <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> | ||||||
|  |           <app-permissions-select i18n-title title="Permissions" formControlName="permissions"></app-permissions-select> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <div class="modal-footer"> | ||||||
|  |       <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||||
|  |       <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> | ||||||
|  |     </div> | ||||||
|  |   </form> | ||||||
| @@ -0,0 +1,32 @@ | |||||||
|  | import { Component } from '@angular/core' | ||||||
|  | import { FormControl, FormGroup } from '@angular/forms' | ||||||
|  | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||||
|  | import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||||
|  | import { PaperlessGroup } from 'src/app/data/paperless-group' | ||||||
|  | import { GroupService } from 'src/app/services/rest/group.service' | ||||||
|  |  | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-group-edit-dialog', | ||||||
|  |   templateUrl: './group-edit-dialog.component.html', | ||||||
|  |   styleUrls: ['./group-edit-dialog.component.scss'], | ||||||
|  | }) | ||||||
|  | export class GroupEditDialogComponent extends EditDialogComponent<PaperlessGroup> { | ||||||
|  |   constructor(service: GroupService, activeModal: NgbActiveModal) { | ||||||
|  |     super(service, activeModal) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getCreateTitle() { | ||||||
|  |     return $localize`Create new user group` | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getEditTitle() { | ||||||
|  |     return $localize`Edit user group` | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getForm(): FormGroup { | ||||||
|  |     return new FormGroup({ | ||||||
|  |       name: new FormControl(''), | ||||||
|  |       permissions: new FormControl(''), | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit- | |||||||
| import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' |  | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-storage-path-edit-dialog', |   selector: 'app-storage-path-edit-dialog', | ||||||
| @@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service' | |||||||
|   styleUrls: ['./storage-path-edit-dialog.component.scss'], |   styleUrls: ['./storage-path-edit-dialog.component.scss'], | ||||||
| }) | }) | ||||||
| export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> { | export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> { | ||||||
|   constructor( |   constructor(service: StoragePathService, activeModal: NgbActiveModal) { | ||||||
|     service: StoragePathService, |     super(service, activeModal) | ||||||
|     activeModal: NgbActiveModal, |  | ||||||
|     toastService: ToastService |  | ||||||
|   ) { |  | ||||||
|     super(service, activeModal, toastService) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get pathHint() { |   get pathHint() { | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | |||||||
| import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||||
| import { PaperlessTag } from 'src/app/data/paperless-tag' | import { PaperlessTag } from 'src/app/data/paperless-tag' | ||||||
| import { TagService } from 'src/app/services/rest/tag.service' | import { TagService } from 'src/app/services/rest/tag.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' |  | ||||||
| import { randomColor } from 'src/app/utils/color' | import { randomColor } from 'src/app/utils/color' | ||||||
| import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||||
|  |  | ||||||
| @@ -14,12 +13,8 @@ import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | |||||||
|   styleUrls: ['./tag-edit-dialog.component.scss'], |   styleUrls: ['./tag-edit-dialog.component.scss'], | ||||||
| }) | }) | ||||||
| export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | ||||||
|   constructor( |   constructor(service: TagService, activeModal: NgbActiveModal) { | ||||||
|     service: TagService, |     super(service, activeModal) | ||||||
|     activeModal: NgbActiveModal, |  | ||||||
|     toastService: ToastService |  | ||||||
|   ) { |  | ||||||
|     super(service, activeModal, toastService) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getCreateTitle() { |   getCreateTitle() { | ||||||
|   | |||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | <form [formGroup]="objectForm" (ngSubmit)="save()"> | ||||||
|  |     <div class="modal-header"> | ||||||
|  |       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||||
|  |       <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||||
|  |       </button> | ||||||
|  |     </div> | ||||||
|  |     <div class="modal-body"> | ||||||
|  |       <div class="row"> | ||||||
|  |         <div class="col"> | ||||||
|  |           <app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text> | ||||||
|  |           <app-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></app-input-text> | ||||||
|  |           <app-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></app-input-text> | ||||||
|  |  | ||||||
|  |           <div class="form-check form-switch"> | ||||||
|  |             <input type="checkbox" class="form-check-input" id="is_active" formControlName="is_active"> | ||||||
|  |             <label class="form-check-label" for="is_active" i18n>Active</label> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|  |           <div class="form-check form-switch"> | ||||||
|  |             <input type="checkbox" class="form-check-input" id="is_superuser" formControlName="is_superuser"> | ||||||
|  |             <label class="form-check-label" for="is_superuser" i18n>Superuser</label> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="col"> | ||||||
|  |           <app-input-select i18n-title title="Groups" [items]="groups" multiple="true" formControlName="groups"></app-input-select> | ||||||
|  |           <app-permissions-select i18n-title title="Permissions" formControlName="permissions"></app-permissions-select> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <div class="modal-footer"> | ||||||
|  |       <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||||
|  |       <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> | ||||||
|  |     </div> | ||||||
|  |   </form> | ||||||
| @@ -0,0 +1,51 @@ | |||||||
|  | import { Component } from '@angular/core' | ||||||
|  | import { FormControl, FormGroup } from '@angular/forms' | ||||||
|  | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||||
|  | import { first } from 'rxjs' | ||||||
|  | import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||||
|  | import { PaperlessGroup } from 'src/app/data/paperless-group' | ||||||
|  | import { PaperlessUser } from 'src/app/data/paperless-user' | ||||||
|  | import { GroupService } from 'src/app/services/rest/group.service' | ||||||
|  | import { UserService } from 'src/app/services/rest/user.service' | ||||||
|  |  | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-user-edit-dialog', | ||||||
|  |   templateUrl: './user-edit-dialog.component.html', | ||||||
|  |   styleUrls: ['./user-edit-dialog.component.scss'], | ||||||
|  | }) | ||||||
|  | export class UserEditDialogComponent extends EditDialogComponent<PaperlessUser> { | ||||||
|  |   groups: PaperlessGroup[] | ||||||
|  |  | ||||||
|  |   constructor( | ||||||
|  |     service: UserService, | ||||||
|  |     activeModal: NgbActiveModal, | ||||||
|  |     groupsService: GroupService | ||||||
|  |   ) { | ||||||
|  |     super(service, activeModal) | ||||||
|  |  | ||||||
|  |     groupsService | ||||||
|  |       .listAll() | ||||||
|  |       .pipe(first()) | ||||||
|  |       .subscribe((result) => (this.groups = result.results)) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getCreateTitle() { | ||||||
|  |     return $localize`Create new user account` | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getEditTitle() { | ||||||
|  |     return $localize`Edit user account` | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getForm(): FormGroup { | ||||||
|  |     return new FormGroup({ | ||||||
|  |       username: new FormControl(''), | ||||||
|  |       first_name: new FormControl(''), | ||||||
|  |       last_name: new FormControl(''), | ||||||
|  |       is_active: new FormControl(''), | ||||||
|  |       is_superuser: new FormControl(''), | ||||||
|  |       groups: new FormControl(''), | ||||||
|  |       permissions: new FormControl(''), | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -11,6 +11,7 @@ | |||||||
|         addTagText="Add item" |         addTagText="Add item" | ||||||
|         i18n-addTagText="Used for both types, correspondents, storage paths" |         i18n-addTagText="Used for both types, correspondents, storage paths" | ||||||
|         [placeholder]="placeholder" |         [placeholder]="placeholder" | ||||||
|  |         [multiple]="multiple" | ||||||
|         bindLabel="name" |         bindLabel="name" | ||||||
|         bindValue="id" |         bindValue="id" | ||||||
|         (change)="onChange(value)" |         (change)="onChange(value)" | ||||||
|   | |||||||
| @@ -44,6 +44,9 @@ export class SelectComponent extends AbstractInputComponent<number> { | |||||||
|   @Input() |   @Input() | ||||||
|   placeholder: string |   placeholder: string | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   multiple: boolean = false | ||||||
|  |  | ||||||
|   @Output() |   @Output() | ||||||
|   createNew = new EventEmitter<string>() |   createNew = new EventEmitter<string>() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | <form [formGroup]="form"> | ||||||
|  |   <label>{{title}}</label> | ||||||
|  |   <ul class="list-group"> | ||||||
|  |     <li class="list-group-item" *ngFor="let type of PermissionType | keyvalue" [formGroupName]="type.key"> | ||||||
|  |       {{type.key}}: | ||||||
|  |  | ||||||
|  |       <div class="form-check form-check-inline form-switch"> | ||||||
|  |         <input type="checkbox" class="form-check-input" id="{{type.key}}_all" formControlName="all"> | ||||||
|  |         <label class="form-check-label" for="{{type.key}}_all" i18n>All</label> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div *ngFor="let action of PermissionAction | keyvalue" class="form-check form-check-inline" [disabled]="isAll(type.key)"> | ||||||
|  |         <input type="checkbox" class="form-check-input" id="{{type.key}}_{{action.key}}" formControlName="{{action.key}}"> | ||||||
|  |         <label class="form-check-label" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label> | ||||||
|  |       </div> | ||||||
|  |     </li> | ||||||
|  |   </ul> | ||||||
|  | </form> | ||||||
| @@ -0,0 +1,79 @@ | |||||||
|  | import { Component, forwardRef, Input, OnInit } from '@angular/core' | ||||||
|  | import { | ||||||
|  |   ControlValueAccessor, | ||||||
|  |   FormControl, | ||||||
|  |   FormGroup, | ||||||
|  |   NG_VALUE_ACCESSOR, | ||||||
|  | } from '@angular/forms' | ||||||
|  | import { | ||||||
|  |   PermissionAction, | ||||||
|  |   PermissionsService, | ||||||
|  |   PermissionType, | ||||||
|  | } from 'src/app/services/permissions.service' | ||||||
|  | import { AbstractInputComponent } from '../input/abstract-input' | ||||||
|  |  | ||||||
|  | @Component({ | ||||||
|  |   providers: [ | ||||||
|  |     { | ||||||
|  |       provide: NG_VALUE_ACCESSOR, | ||||||
|  |       useExisting: forwardRef(() => PermissionsSelectComponent), | ||||||
|  |       multi: true, | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  |   selector: 'app-permissions-select', | ||||||
|  |   templateUrl: './permissions-select.component.html', | ||||||
|  |   styleUrls: ['./permissions-select.component.scss'], | ||||||
|  | }) | ||||||
|  | export class PermissionsSelectComponent | ||||||
|  |   implements OnInit, ControlValueAccessor | ||||||
|  | { | ||||||
|  |   PermissionType = PermissionType | ||||||
|  |   PermissionAction = PermissionAction | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   title: string = 'Permissions' | ||||||
|  |  | ||||||
|  |   permissions: string[] | ||||||
|  |  | ||||||
|  |   form = new FormGroup({}) | ||||||
|  |  | ||||||
|  |   constructor(private readonly permissionsService: PermissionsService) { | ||||||
|  |     for (const type in PermissionType) { | ||||||
|  |       const control = new FormGroup({}) | ||||||
|  |       control.addControl('all', new FormControl(null)) | ||||||
|  |       for (const action in PermissionAction) { | ||||||
|  |         control.addControl(action, new FormControl(null)) | ||||||
|  |       } | ||||||
|  |       this.form.addControl(type, control) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   writeValue(permissions: string[]): void { | ||||||
|  |     this.permissions = permissions | ||||||
|  |     this.permissions.forEach((permissionStr) => { | ||||||
|  |       const { actionKey, typeKey } = | ||||||
|  |         this.permissionsService.getPermissionKeys(permissionStr) | ||||||
|  |  | ||||||
|  |       if (actionKey && typeKey) { | ||||||
|  |         if (this.form.get(typeKey)?.get(actionKey)) { | ||||||
|  |           this.form.get(typeKey).get(actionKey).setValue(true) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |   registerOnChange(fn: any): void { | ||||||
|  |     throw new Error('Method not implemented.') | ||||||
|  |   } | ||||||
|  |   registerOnTouched(fn: any): void { | ||||||
|  |     throw new Error('Method not implemented.') | ||||||
|  |   } | ||||||
|  |   setDisabledState?(isDisabled: boolean): void { | ||||||
|  |     throw new Error('Method not implemented.') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ngOnInit(): void {} | ||||||
|  |  | ||||||
|  |   isAll(key: string): boolean { | ||||||
|  |     return this.form.get(key).get('all').value == true | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -221,6 +221,82 @@ | |||||||
|  |  | ||||||
|       </ng-template> |       </ng-template> | ||||||
|     </li> |     </li> | ||||||
|  |  | ||||||
|  |     <li [ngbNavItem]="SettingsNavIDs.UsersGroups" *ifPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" (mouseover)="maybeInitializeTab(SettingsNavIDs.UsersGroups)" (focusin)="maybeInitializeTab(SettingsNavIDs.UsersGroups)"> | ||||||
|  |       <a ngbNavLink i18n>Users & Groups</a> | ||||||
|  |       <ng-template ngbNavContent> | ||||||
|  |  | ||||||
|  |       <ng-container *ngIf="users && groups"> | ||||||
|  |         <h4 class="d-flex"> | ||||||
|  |           <ng-container i18n>Users</ng-container> | ||||||
|  |           <button type="button" class="btn btn-sm btn-primary ms-4" (click)="editUser()" i18n>Add User</button> | ||||||
|  |         </h4> | ||||||
|  |         <ul class="list-group" formGroupName="usersGroup"> | ||||||
|  |  | ||||||
|  |           <li class="list-group-item"> | ||||||
|  |             <div class="row"> | ||||||
|  |               <div class="col" i18n>Username</div> | ||||||
|  |               <div class="col" i18n>Name</div> | ||||||
|  |               <div class="col" i18n>Groups</div> | ||||||
|  |               <div class="col" i18n>Actions</div> | ||||||
|  |             </div> | ||||||
|  |           </li> | ||||||
|  |  | ||||||
|  |           <li *ngFor="let user of users" class="list-group-item" [formGroupName]="user.id"> | ||||||
|  |             <div class="row"> | ||||||
|  |               <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editUser(user)">{{user.username}}</button></div> | ||||||
|  |               <div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div> | ||||||
|  |               <div class="col d-flex align-items-center">{{user.groups}}</div> | ||||||
|  |               <div class="col"> | ||||||
|  |                 <div class="btn-group"> | ||||||
|  |                   <button class="btn btn-sm btn-primary" type="button" (click)="editUser(user)" i18n>Edit</button> | ||||||
|  |                   <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" i18n>Delete</button> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </li> | ||||||
|  |         </ul> | ||||||
|  |  | ||||||
|  |         <h4 class="mt-4 d-flex"> | ||||||
|  |           <ng-container i18n>Groups</ng-container> | ||||||
|  |           <button type="button" class="btn btn-sm btn-primary ms-4" (click)="editGroup()" i18n>Add Group</button> | ||||||
|  |         </h4> | ||||||
|  |         <ul *ngIf="groups.length > 0" class="list-group" formGroupName="groupsGroup"> | ||||||
|  |  | ||||||
|  |           <li class="list-group-item"> | ||||||
|  |             <div class="row"> | ||||||
|  |               <div class="col" i18n>Name</div> | ||||||
|  |               <div class="col"></div> | ||||||
|  |               <div class="col"></div> | ||||||
|  |               <div class="col" i18n>Actions</div> | ||||||
|  |             </div> | ||||||
|  |           </li> | ||||||
|  |  | ||||||
|  |           <li *ngFor="let group of groups" class="list-group-item" [formGroupName]="group.id"> | ||||||
|  |             <div class="row"> | ||||||
|  |               <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)">{{group.name}}</button></div> | ||||||
|  |               <div class="col"></div> | ||||||
|  |               <div class="col"></div> | ||||||
|  |               <div class="col"> | ||||||
|  |                 <div class="btn-group"> | ||||||
|  |                   <button class="btn btn-sm btn-primary" type="button" (click)="editGroup(group)" i18n>Edit</button> | ||||||
|  |                   <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" i18n>Delete</button> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </li> | ||||||
|  |         </ul> | ||||||
|  |  | ||||||
|  |         <div *ngIf="groups.length == 0">No groups defined</div> | ||||||
|  |       </ng-container> | ||||||
|  |  | ||||||
|  |       <div *ngIf="!users || !groups"> | ||||||
|  |         <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||||
|  |         <div class="visually-hidden" i18n>Loading...</div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       </ng-template> | ||||||
|  |     </li> | ||||||
|   </ul> |   </ul> | ||||||
|  |  | ||||||
|   <div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div> |   <div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div> | ||||||
|   | |||||||
| @@ -30,8 +30,15 @@ import { ActivatedRoute } from '@angular/router' | |||||||
| import { ViewportScroller } from '@angular/common' | import { ViewportScroller } from '@angular/common' | ||||||
| import { TourService } from 'ngx-ui-tour-ng-bootstrap' | import { TourService } from 'ngx-ui-tour-ng-bootstrap' | ||||||
| import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' | import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' | ||||||
| import { NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap' | import { NgbModal, NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap' | ||||||
| import { Results } from 'src/app/data/results' | import { Results } from 'src/app/data/results' | ||||||
|  | import { UserService } from 'src/app/services/rest/user.service' | ||||||
|  | import { GroupService } from 'src/app/services/rest/group.service' | ||||||
|  | import { PaperlessUser } from 'src/app/data/paperless-user' | ||||||
|  | import { PaperlessGroup } from 'src/app/data/paperless-group' | ||||||
|  | import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component' | ||||||
|  | import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' | ||||||
|  | import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component' | ||||||
|  |  | ||||||
| enum SettingsNavIDs { | enum SettingsNavIDs { | ||||||
|   General = 1, |   General = 1, | ||||||
| @@ -54,6 +61,8 @@ export class SettingsComponent | |||||||
|   activeNavID: number |   activeNavID: number | ||||||
|  |  | ||||||
|   savedViewGroup = new FormGroup({}) |   savedViewGroup = new FormGroup({}) | ||||||
|  |   usersGroup = new FormGroup({}) | ||||||
|  |   groupsGroup = new FormGroup({}) | ||||||
|  |  | ||||||
|   settingsForm = new FormGroup({ |   settingsForm = new FormGroup({ | ||||||
|     bulkEditConfirmationDialogs: new FormControl(null), |     bulkEditConfirmationDialogs: new FormControl(null), | ||||||
| @@ -75,6 +84,8 @@ export class SettingsComponent | |||||||
|     notificationsConsumerSuppressOnDashboard: new FormControl(null), |     notificationsConsumerSuppressOnDashboard: new FormControl(null), | ||||||
|     commentsEnabled: new FormControl(null), |     commentsEnabled: new FormControl(null), | ||||||
|     updateCheckingEnabled: new FormControl(null), |     updateCheckingEnabled: new FormControl(null), | ||||||
|  |     usersGroup: this.usersGroup, | ||||||
|  |     groupsGroup: this.groupsGroup, | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   savedViews: PaperlessSavedView[] |   savedViews: PaperlessSavedView[] | ||||||
| @@ -86,6 +97,9 @@ export class SettingsComponent | |||||||
|   unsubscribeNotifier: Subject<any> = new Subject() |   unsubscribeNotifier: Subject<any> = new Subject() | ||||||
|   savePending: boolean = false |   savePending: boolean = false | ||||||
|  |  | ||||||
|  |   users: PaperlessUser[] | ||||||
|  |   groups: PaperlessGroup[] | ||||||
|  |  | ||||||
|   get computedDateLocale(): string { |   get computedDateLocale(): string { | ||||||
|     return ( |     return ( | ||||||
|       this.settingsForm.value.dateLocale || |       this.settingsForm.value.dateLocale || | ||||||
| @@ -102,7 +116,10 @@ export class SettingsComponent | |||||||
|     @Inject(LOCALE_ID) public currentLocale: string, |     @Inject(LOCALE_ID) public currentLocale: string, | ||||||
|     private viewportScroller: ViewportScroller, |     private viewportScroller: ViewportScroller, | ||||||
|     private activatedRoute: ActivatedRoute, |     private activatedRoute: ActivatedRoute, | ||||||
|     public readonly tourService: TourService |     public readonly tourService: TourService, | ||||||
|  |     private usersService: UserService, | ||||||
|  |     private groupsService: GroupService, | ||||||
|  |     private modalService: NgbModal | ||||||
|   ) { |   ) { | ||||||
|     super() |     super() | ||||||
|     this.settings.settingsSaved.subscribe(() => { |     this.settings.settingsSaved.subscribe(() => { | ||||||
| @@ -159,6 +176,8 @@ export class SettingsComponent | |||||||
|       updateCheckingEnabled: this.settings.get( |       updateCheckingEnabled: this.settings.get( | ||||||
|         SETTINGS_KEYS.UPDATE_CHECKING_ENABLED |         SETTINGS_KEYS.UPDATE_CHECKING_ENABLED | ||||||
|       ), |       ), | ||||||
|  |       usersGroup: {}, | ||||||
|  |       groupsGroup: {}, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -176,6 +195,18 @@ export class SettingsComponent | |||||||
|         this.savedViews = r.results |         this.savedViews = r.results | ||||||
|         this.initialize() |         this.initialize() | ||||||
|       }) |       }) | ||||||
|  |     } else if ( | ||||||
|  |       (navID == SettingsNavIDs.UsersGroups && !this.users) || | ||||||
|  |       !this.groups | ||||||
|  |     ) { | ||||||
|  |       this.usersService.listAll().subscribe((r) => { | ||||||
|  |         this.users = r.results | ||||||
|  |  | ||||||
|  |         this.groupsService.listAll().subscribe((r) => { | ||||||
|  |           this.groups = r.results | ||||||
|  |           this.initialize() | ||||||
|  |         }) | ||||||
|  |       }) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -204,6 +235,50 @@ export class SettingsComponent | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (this.users && this.groups) { | ||||||
|  |       for (let user of this.users) { | ||||||
|  |         storeData.usersGroup[user.id.toString()] = { | ||||||
|  |           id: user.id, | ||||||
|  |           username: user.username, | ||||||
|  |           first_name: user.first_name, | ||||||
|  |           last_name: user.last_name, | ||||||
|  |           is_active: user.is_active, | ||||||
|  |           is_superuser: user.is_superuser, | ||||||
|  |           groups: user.groups, | ||||||
|  |           permissions: user.permissions, | ||||||
|  |         } | ||||||
|  |         this.usersGroup.addControl( | ||||||
|  |           user.id.toString(), | ||||||
|  |           new FormGroup({ | ||||||
|  |             id: new FormControl(null), | ||||||
|  |             username: new FormControl(null), | ||||||
|  |             first_name: new FormControl(null), | ||||||
|  |             last_name: new FormControl(null), | ||||||
|  |             is_active: new FormControl(null), | ||||||
|  |             is_superuser: new FormControl(null), | ||||||
|  |             groups: new FormControl(null), | ||||||
|  |             permissions: new FormControl(null), | ||||||
|  |           }) | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       for (let group of this.groups) { | ||||||
|  |         storeData.groupsGroup[group.id.toString()] = { | ||||||
|  |           id: group.id, | ||||||
|  |           name: group.name, | ||||||
|  |           permissions: group.permissions, | ||||||
|  |         } | ||||||
|  |         this.groupsGroup.addControl( | ||||||
|  |           group.id.toString(), | ||||||
|  |           new FormGroup({ | ||||||
|  |             id: new FormControl(null), | ||||||
|  |             name: new FormControl(null), | ||||||
|  |             permissions: new FormControl(null), | ||||||
|  |           }) | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     this.store = new BehaviorSubject(storeData) |     this.store = new BehaviorSubject(storeData) | ||||||
|  |  | ||||||
|     this.storeSub = this.store.asObservable().subscribe((state) => { |     this.storeSub = this.store.asObservable().subscribe((state) => { | ||||||
| @@ -400,4 +475,86 @@ export class SettingsComponent | |||||||
|   clearThemeColor() { |   clearThemeColor() { | ||||||
|     this.settingsForm.get('themeColor').patchValue('') |     this.settingsForm.get('themeColor').patchValue('') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   editUser(user: PaperlessUser) { | ||||||
|  |     var modal = this.modalService.open(UserEditDialogComponent, { | ||||||
|  |       backdrop: 'static', | ||||||
|  |       size: 'xl', | ||||||
|  |     }) | ||||||
|  |     modal.componentInstance.dialogMode = user ? 'edit' : 'create' | ||||||
|  |     modal.componentInstance.object = user | ||||||
|  |     modal.componentInstance.success | ||||||
|  |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|  |       .subscribe({ | ||||||
|  |         next: (newUser) => { | ||||||
|  |           this.toastService.showInfo( | ||||||
|  |             $localize`Saved user "${newUser.username}".` | ||||||
|  |           ) | ||||||
|  |           this.usersService.listAll().subscribe((r) => { | ||||||
|  |             this.users = r.results | ||||||
|  |             this.initialize() | ||||||
|  |           }) | ||||||
|  |         }, | ||||||
|  |         error: (e) => { | ||||||
|  |           this.toastService.showError( | ||||||
|  |             $localize`Error saving user: ${e.toString()}.` | ||||||
|  |           ) | ||||||
|  |         }, | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   deleteUser(user: PaperlessUser) { | ||||||
|  |     let modal = this.modalService.open(ConfirmDialogComponent, { | ||||||
|  |       backdrop: 'static', | ||||||
|  |     }) | ||||||
|  |     modal.componentInstance.title = $localize`Confirm delete user account` | ||||||
|  |     modal.componentInstance.messageBold = $localize`This operation will permanently this user account.` | ||||||
|  |     modal.componentInstance.message = $localize`This operation cannot be undone.` | ||||||
|  |     modal.componentInstance.btnClass = 'btn-danger' | ||||||
|  |     modal.componentInstance.btnCaption = $localize`Proceed` | ||||||
|  |     modal.componentInstance.confirmClicked.subscribe(() => { | ||||||
|  |       modal.componentInstance.buttonsEnabled = false | ||||||
|  |       this.usersService.delete(user) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   editGroup(group: PaperlessGroup) { | ||||||
|  |     var modal = this.modalService.open(GroupEditDialogComponent, { | ||||||
|  |       backdrop: 'static', | ||||||
|  |       size: 'lg', | ||||||
|  |     }) | ||||||
|  |     modal.componentInstance.dialogMode = group ? 'edit' : 'create' | ||||||
|  |     modal.componentInstance.object = group | ||||||
|  |     modal.componentInstance.success | ||||||
|  |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|  |       .subscribe({ | ||||||
|  |         next: (newGroup) => { | ||||||
|  |           this.toastService.showInfo($localize`Saved group "${newGroup.name}".`) | ||||||
|  |           this.groupsService.listAll().subscribe((r) => { | ||||||
|  |             this.groups = r.results | ||||||
|  |             this.initialize() | ||||||
|  |           }) | ||||||
|  |         }, | ||||||
|  |         error: (e) => { | ||||||
|  |           this.toastService.showError( | ||||||
|  |             $localize`Error saving group: ${e.toString()}.` | ||||||
|  |           ) | ||||||
|  |         }, | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   deleteGroup(group: PaperlessGroup) { | ||||||
|  |     let modal = this.modalService.open(ConfirmDialogComponent, { | ||||||
|  |       backdrop: 'static', | ||||||
|  |     }) | ||||||
|  |     modal.componentInstance.title = $localize`Confirm delete user group` | ||||||
|  |     modal.componentInstance.messageBold = $localize`This operation will permanently this user group.` | ||||||
|  |     modal.componentInstance.message = $localize`This operation cannot be undone.` | ||||||
|  |     modal.componentInstance.btnClass = 'btn-danger' | ||||||
|  |     modal.componentInstance.btnCaption = $localize`Proceed` | ||||||
|  |     modal.componentInstance.confirmClicked.subscribe(() => { | ||||||
|  |       modal.componentInstance.buttonsEnabled = false | ||||||
|  |       this.groupsService.delete(group) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ export enum PermissionType { | |||||||
|   Log = 'admin.%s_logentry', |   Log = 'admin.%s_logentry', | ||||||
|   MailAccount = 'paperless_mail.%s_mailaccount', |   MailAccount = 'paperless_mail.%s_mailaccount', | ||||||
|   MailRule = 'paperless_mail.%s_mailrule', |   MailRule = 'paperless_mail.%s_mailrule', | ||||||
|   Auth = 'auth.%s_user', |   User = 'auth.%s_user', | ||||||
|   Admin = 'admin.%s_logentry', |   Admin = 'admin.%s_logentry', | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -46,4 +46,30 @@ export class PermissionsService { | |||||||
|   private getPermissionCode(permission: PaperlessPermission): string { |   private getPermissionCode(permission: PaperlessPermission): string { | ||||||
|     return permission.type.replace('%s', permission.action) |     return permission.type.replace('%s', permission.action) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public getPermissionKeys(permissionStr: string): { | ||||||
|  |     actionKey: string | ||||||
|  |     typeKey: string | ||||||
|  |   } { | ||||||
|  |     const matches = permissionStr.match(/\.(.+)_/) | ||||||
|  |     let typeKey | ||||||
|  |     let actionKey | ||||||
|  |     if (matches?.length > 0) { | ||||||
|  |       const action = matches[1] | ||||||
|  |       const actionIndex = Object.values(PermissionAction).indexOf( | ||||||
|  |         action as PermissionAction | ||||||
|  |       ) | ||||||
|  |       if (actionIndex > -1) { | ||||||
|  |         actionKey = Object.keys(PermissionAction)[actionIndex] | ||||||
|  |       } | ||||||
|  |       const typeIndex = Object.values(PermissionType).indexOf( | ||||||
|  |         permissionStr.replace(action, '%s') as PermissionType | ||||||
|  |       ) | ||||||
|  |       if (typeIndex > -1) { | ||||||
|  |         typeKey = Object.keys(PermissionType)[typeIndex] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return { actionKey, typeKey } | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ class FaviconView(View): | |||||||
| class UserViewSet(ModelViewSet): | class UserViewSet(ModelViewSet): | ||||||
|     model = User |     model = User | ||||||
|  |  | ||||||
|     queryset = User.objects.order_by(Lower("username")) |     queryset = User.objects.exclude(username="consumer").order_by(Lower("username")) | ||||||
|  |  | ||||||
|     serializer_class = UserSerializer |     serializer_class = UserSerializer | ||||||
|     pagination_class = StandardPagination |     pagination_class = StandardPagination | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Michael Shamoon
					Michael Shamoon