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,