From c73688d16772f456d06e942cccc62e99bfa87d28 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 7 Dec 2022 14:55:40 -0800 Subject: [PATCH] add share to c/dt/t/sp, refactor share input, ifOwner directive --- src-ui/src/app/app.module.ts | 4 ++ .../correspondent-edit-dialog.component.html | 10 ++++ .../correspondent-edit-dialog.component.ts | 4 ++ .../document-type-edit-dialog.component.html | 8 ++++ .../document-type-edit-dialog.component.ts | 4 ++ .../edit-dialog/edit-dialog.component.ts | 16 ++++++- .../storage-path-edit-dialog.component.html | 8 ++++ .../storage-path-edit-dialog.component.ts | 4 ++ .../tag-edit-dialog.component.html | 9 ++++ .../tag-edit-dialog.component.ts | 4 ++ .../share-user/share-user.component.html | 15 ++++++ .../share-user/share-user.component.scss | 0 .../input/share-user/share-user.component.ts | 47 +++++++++++++++++++ .../document-detail.component.html | 6 +-- .../document-detail.component.ts | 10 +--- .../src/app/data/object-with-permissions.ts | 2 +- .../src/app/directives/if-owner.directive.ts | 42 +++++++++++++++++ src-ui/src/app/services/settings.service.ts | 17 +++++-- 18 files changed, 192 insertions(+), 18 deletions(-) create mode 100644 src-ui/src/app/components/common/input/share-user/share-user.component.html create mode 100644 src-ui/src/app/components/common/input/share-user/share-user.component.scss create mode 100644 src-ui/src/app/components/common/input/share-user/share-user.component.ts create mode 100644 src-ui/src/app/directives/if-owner.directive.ts diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 65a7cda14..183a6ad25 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -104,6 +104,8 @@ import localeSr from '@angular/common/locales/sr' import localeSv from '@angular/common/locales/sv' import localeTr from '@angular/common/locales/tr' import localeZh from '@angular/common/locales/zh' +import { ShareUserComponent } from './components/common/input/share-user/share-user.component' +import { IfOwnerDirective } from './directives/if-owner.directive' registerLocaleData(localeBe) registerLocaleData(localeCs) @@ -195,6 +197,8 @@ function initializeApp(settings: SettingsService) { PermissionsSelectComponent, MailAccountEditDialogComponent, MailRuleEditDialogComponent, + ShareUserComponent, + IfOwnerDirective, ], imports: [ BrowserModule, diff --git a/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html index a11b6363e..857bd2f1c 100644 --- a/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html @@ -5,10 +5,20 @@ </button> </div> <div class="modal-body"> + <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select> <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check> + + <div *ifOwner="object.owner"> + <h5 i18n>Permissions</h5> + <div formGroupName="set_permissions"> + <app-share-user type="view" formControlName="view"></app-share-user> + <app-share-user type="change" formControlName="change"></app-share-user> + </div> + </div> + </div> <div class="modal-footer"> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> diff --git a/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.ts index 7361e5e4b..a7c3eb606 100644 --- a/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.ts @@ -30,6 +30,10 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM), match: new FormControl(''), is_insensitive: new FormControl(true), + set_permissions: new FormGroup({ + view: new FormControl(null), + change: new FormControl(null), + }), }) } } diff --git a/src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html index 03d17c35a..b7d7f1335 100644 --- a/src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html @@ -11,6 +11,14 @@ <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check> + <div *ifOwner="object.owner"> + <h5 i18n>Permissions</h5> + <div formGroupName="set_permissions"> + <app-share-user type="view" formControlName="view"></app-share-user> + <app-share-user type="change" formControlName="change"></app-share-user> + </div> + </div> + </div> <div class="modal-footer"> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> diff --git a/src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.ts index d565e66e1..ef4d0a864 100644 --- a/src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.ts @@ -30,6 +30,10 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM), match: new FormControl(''), is_insensitive: new FormControl(true), + set_permissions: new FormGroup({ + view: new FormControl(null), + change: new FormControl(null), + }), }) } } diff --git a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts index 9bf141e78..a9133f60f 100644 --- a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts @@ -4,11 +4,13 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { Observable } from 'rxjs' import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model' import { ObjectWithId } from 'src/app/data/object-with-id' +import { ObjectWithPermissions } from 'src/app/data/object-with-permissions' import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service' @Directive() -export abstract class EditDialogComponent<T extends ObjectWithId> - implements OnInit +export abstract class EditDialogComponent< + T extends ObjectWithPermissions | ObjectWithId +> implements OnInit { constructor( private service: AbstractPaperlessService<T>, @@ -36,6 +38,16 @@ export abstract class EditDialogComponent<T extends ObjectWithId> ngOnInit(): void { if (this.object != null) { + if (this.object['permissions']) { + this.object['set_permissions'] = { + view: (this.object as ObjectWithPermissions).permissions + .filter((p) => (p[1] as string).includes('view')) + .map((p) => p[0]), + change: (this.object as ObjectWithPermissions).permissions + .filter((p) => (p[1] as string).includes('change')) + .map((p) => p[0]), + } + } this.objectForm.patchValue(this.object) } diff --git a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html index 280c101a6..e122b8c00 100644 --- a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html @@ -16,6 +16,14 @@ <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check> + <div *ifOwner="object.owner"> + <h5 i18n>Permissions</h5> + <div formGroupName="set_permissions"> + <app-share-user type="view" formControlName="view"></app-share-user> + <app-share-user type="change" formControlName="change"></app-share-user> + </div> + </div> + </div> <div class="modal-footer"> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> diff --git a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts index 1dfef00c5..7c4898703 100644 --- a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts @@ -41,6 +41,10 @@ export class StoragePathEditDialogComponent extends EditDialogComponent<Paperles matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM), match: new FormControl(''), is_insensitive: new FormControl(true), + set_permissions: new FormGroup({ + view: new FormControl(null), + change: new FormControl(null), + }), }) } } diff --git a/src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html index 6ea3901a7..a4a7fbbf5 100644 --- a/src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html @@ -13,6 +13,15 @@ <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select> <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check> + + <div *ifOwner="object.owner"> + <h5 i18n>Permissions</h5> + <div formGroupName="set_permissions"> + <app-share-user type="view" formControlName="view"></app-share-user> + <app-share-user type="change" formControlName="change"></app-share-user> + </div> + </div> + </div> <div class="modal-footer"> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> diff --git a/src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.ts index db106d990..0414052a0 100644 --- a/src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.ts @@ -33,6 +33,10 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM), match: new FormControl(''), is_insensitive: new FormControl(true), + set_permissions: new FormGroup({ + view: new FormControl(null), + change: new FormControl(null), + }), }) } } diff --git a/src-ui/src/app/components/common/input/share-user/share-user.component.html b/src-ui/src/app/components/common/input/share-user/share-user.component.html new file mode 100644 index 000000000..9022b1a99 --- /dev/null +++ b/src-ui/src/app/components/common/input/share-user/share-user.component.html @@ -0,0 +1,15 @@ +<div class="mb-3 paperless-input-select"> + <label class="form-label" [for]="inputId">{{title}}</label> + <div> + <ng-select name="inputId" [(ngModel)]="value" + [disabled]="disabled" + clearable="true" + [items]="users" + multiple="true" + bindLabel="username" + bindValue="id" + (change)="onChange(value)"> + </ng-select> + </div> + <small *ngIf="hint" class="form-text text-muted">{{hint}}</small> + </div> diff --git a/src-ui/src/app/components/common/input/share-user/share-user.component.scss b/src-ui/src/app/components/common/input/share-user/share-user.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/common/input/share-user/share-user.component.ts b/src-ui/src/app/components/common/input/share-user/share-user.component.ts new file mode 100644 index 000000000..d132b3964 --- /dev/null +++ b/src-ui/src/app/components/common/input/share-user/share-user.component.ts @@ -0,0 +1,47 @@ +import { Component, forwardRef, Input, OnInit } from '@angular/core' +import { NG_VALUE_ACCESSOR } from '@angular/forms' +import { first } from 'rxjs/operators' +import { PaperlessUser } from 'src/app/data/paperless-user' +import { UserService } from 'src/app/services/rest/user.service' +import { AbstractInputComponent } from '../abstract-input' + +@Component({ + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ShareUserComponent), + multi: true, + }, + ], + selector: 'app-share-user', + templateUrl: './share-user.component.html', + styleUrls: ['./share-user.component.scss'], +}) +export class ShareUserComponent + extends AbstractInputComponent<PaperlessUser> + implements OnInit +{ + users: PaperlessUser[] + + @Input() + type: string + + constructor(userService: UserService) { + super() + userService + .listAll() + .pipe(first()) + .subscribe((result) => (this.users = result.results)) + } + + ngOnInit(): void { + if (this.type == 'view') { + this.title = $localize`Users can view` + } else if (this.type == 'change') { + this.title = $localize`Users can edit` + this.hint = $localize`Edit permissions also grant viewing permissions` + } + + super.ngOnInit() + } +} diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 9ffc00dd4..96a4e471f 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -178,12 +178,12 @@ </ng-template> </li> - <li [ngbNavItem]="6"> + <li [ngbNavItem]="6" *ifOwner="document?.owner"> <a ngbNavLink i18n>Permissions</a> <ng-template ngbNavContent> <div formGroupName="set_permissions"> - <app-input-select i18n-title title="Users can view" [items]="users" [bindLabel]="'username'" multiple="true" formControlName="view"></app-input-select> - <app-input-select i18n-title title="Users can edit" [items]="users" [bindLabel]="'username'" multiple="true" formControlName="change"></app-input-select> + <app-share-user type="view" formControlName="view"></app-share-user> + <app-share-user type="change" formControlName="change"></app-share-user> </div> </ng-template> </li> diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 72f4be70b..07f03ece6 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -40,7 +40,6 @@ import { PermissionsService, PermissionType, } from 'src/app/services/permissions.service' -import { UserService } from 'src/app/services/rest/user.service' import { PaperlessUser } from 'src/app/data/paperless-user' @Component({ @@ -75,7 +74,6 @@ export class DocumentDetailComponent correspondents: PaperlessCorrespondent[] documentTypes: PaperlessDocumentType[] storagePaths: PaperlessStoragePath[] - users: PaperlessUser[] documentForm: FormGroup = new FormGroup({ title: new FormControl(''), @@ -134,8 +132,7 @@ export class DocumentDetailComponent private toastService: ToastService, private settings: SettingsService, private storagePathService: StoragePathService, - private permissionsService: PermissionsService, - private userService: UserService + private permissionsService: PermissionsService ) {} titleKeyUp(event) { @@ -175,11 +172,6 @@ export class DocumentDetailComponent .pipe(first()) .subscribe((result) => (this.storagePaths = result.results)) - this.userService - .listAll() - .pipe(first()) - .subscribe((result) => (this.users = result.results)) - this.route.paramMap .pipe( takeUntil(this.unsubscribeNotifier), diff --git a/src-ui/src/app/data/object-with-permissions.ts b/src-ui/src/app/data/object-with-permissions.ts index 786cb16fa..a95ada157 100644 --- a/src-ui/src/app/data/object-with-permissions.ts +++ b/src-ui/src/app/data/object-with-permissions.ts @@ -2,7 +2,7 @@ import { ObjectWithId } from './object-with-id' import { PaperlessUser } from './paperless-user' export interface ObjectWithPermissions extends ObjectWithId { - user?: PaperlessUser + owner?: PaperlessUser permissions?: Array<[number, string]> } diff --git a/src-ui/src/app/directives/if-owner.directive.ts b/src-ui/src/app/directives/if-owner.directive.ts new file mode 100644 index 000000000..51a2c910f --- /dev/null +++ b/src-ui/src/app/directives/if-owner.directive.ts @@ -0,0 +1,42 @@ +import { + Directive, + Input, + OnChanges, + OnInit, + TemplateRef, + ViewContainerRef, +} from '@angular/core' +import { PaperlessUser } from '../data/paperless-user' +import { SettingsService } from '../services/settings.service' + +@Directive({ + selector: '[ifOwner]', +}) +export class IfOwnerDirective implements OnInit, OnChanges { + // The role the user must have + @Input() + ifOwner: PaperlessUser + + /** + * @param {ViewContainerRef} viewContainerRef -- The location where we need to render the templateRef + * @param {TemplateRef<any>} templateRef -- The templateRef to be potentially rendered + * @param {PermissionsService} permissionsService -- Will give us access to the permissions a user has + */ + constructor( + private viewContainerRef: ViewContainerRef, + private templateRef: TemplateRef<any>, + private settings: SettingsService + ) {} + + public ngOnInit(): void { + if (!this.ifOwner || this.ifOwner?.id === this.settings.currentUser.id) { + this.viewContainerRef.createEmbeddedView(this.templateRef) + } else { + this.viewContainerRef.clear() + } + } + + public ngOnChanges(): void { + this.ngOnInit() + } +} diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index eec923c8d..fbf8b9320 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -23,6 +23,7 @@ import { SETTINGS, SETTINGS_KEYS, } from '../data/paperless-uisettings' +import { PaperlessUser } from '../data/paperless-user' import { PermissionsService } from './permissions.service' import { SavedViewService } from './rest/saved-view.service' import { ToastService } from './toast.service' @@ -46,8 +47,7 @@ export class SettingsService { protected baseUrl: string = environment.apiBaseUrl + 'ui_settings/' private settings: Object = {} - - public displayName: string + currentUser: PaperlessUser public settingsSaved: EventEmitter<any> = new EventEmitter() @@ -75,12 +75,23 @@ export class SettingsService { // to update lang cookie if (this.settings['language']?.length) this.setLanguage(this.settings['language']) - this.displayName = uisettings.display_name.trim() + this.currentUser = { + id: uisettings['user_id'], + username: uisettings['username'], + } this.permissionsService.initialize(uisettings.permissions) }) ) } + get displayName(): string { + return ( + this.currentUser.first_name ?? + this.currentUser.username ?? + '' + ).trim() + } + public updateAppearanceSettings( darkModeUseSystem = null, darkModeEnabled = null,