mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Add frontend owner filtering
Add owner to doc cards, table Frontend testing for owner filtering
This commit is contained in:
		| @@ -150,7 +150,7 @@ describe('documents-list', () => { | ||||
|         cy.contains('button', 'Corresp 11').click() | ||||
|         cy.contains('label', 'Exclude').click() | ||||
|       }) | ||||
|     cy.contains('One document') | ||||
|     cy.contains('3 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should apply tags', () => { | ||||
|   | ||||
| @@ -190,6 +190,36 @@ describe('documents query params', () => { | ||||
|           response.count = response.results.length | ||||
|         } | ||||
|  | ||||
|         if (req.query.hasOwnProperty('owner__id')) { | ||||
|           response.results = ( | ||||
|             documentsJson.results as Array<PaperlessDocument> | ||||
|           ).filter((d) => d.owner == req.query['owner__id']) | ||||
|           response.count = response.results.length | ||||
|         } else if (req.query.hasOwnProperty('owner__id__in')) { | ||||
|           const owners = req.query['owner__id__in'] | ||||
|             .toString() | ||||
|             .split(',') | ||||
|             .map((o) => parseInt(o)) | ||||
|           response.results = ( | ||||
|             documentsJson.results as Array<PaperlessDocument> | ||||
|           ).filter((d) => owners.includes(d.owner)) | ||||
|           response.count = response.results.length | ||||
|         } else if (req.query.hasOwnProperty('owner__id__none')) { | ||||
|           const owners = req.query['owner__id__none'] | ||||
|             .toString() | ||||
|             .split(',') | ||||
|             .map((o) => parseInt(o)) | ||||
|           response.results = ( | ||||
|             documentsJson.results as Array<PaperlessDocument> | ||||
|           ).filter((d) => !owners.includes(d.owner)) | ||||
|           response.count = response.results.length | ||||
|         } else if (req.query.hasOwnProperty('owner__isnull')) { | ||||
|           response.results = ( | ||||
|             documentsJson.results as Array<PaperlessDocument> | ||||
|           ).filter((d) => d.owner === null) | ||||
|           response.count = response.results.length | ||||
|         } | ||||
|  | ||||
|         req.reply(response) | ||||
|       }) | ||||
|     }) | ||||
| @@ -202,7 +232,7 @@ describe('documents query params', () => { | ||||
|  | ||||
|   it('should show a list of documents reverse sorted by created', () => { | ||||
|     cy.visit('/documents?sort=created&reverse=true') | ||||
|     cy.get('app-document-card-small').first().contains('sit amet') | ||||
|     cy.get('app-document-card-small').first().contains('Doc 6') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents sorted by added', () => { | ||||
| @@ -212,7 +242,7 @@ describe('documents query params', () => { | ||||
|  | ||||
|   it('should show a list of documents reverse sorted by added', () => { | ||||
|     cy.visit('/documents?sort=added&reverse=true') | ||||
|     cy.get('app-document-card-small').first().contains('sit amet') | ||||
|     cy.get('app-document-card-small').first().contains('Doc 6') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by any tags', () => { | ||||
| @@ -222,12 +252,12 @@ describe('documents query params', () => { | ||||
|  | ||||
|   it('should show a list of documents filtered by excluded tags', () => { | ||||
|     cy.visit('/documents?sort=created&reverse=true&tags__id__none=2,4') | ||||
|     cy.contains('One document') | ||||
|     cy.contains('3 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by no tags', () => { | ||||
|     cy.visit('/documents?sort=created&reverse=true&is_tagged=0') | ||||
|     cy.contains('One document') | ||||
|     cy.contains('3 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by document type', () => { | ||||
| @@ -242,7 +272,7 @@ describe('documents query params', () => { | ||||
|  | ||||
|   it('should show a list of documents filtered by no document type', () => { | ||||
|     cy.visit('/documents?sort=created&reverse=true&document_type__isnull=1') | ||||
|     cy.contains('One document') | ||||
|     cy.contains('3 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by correspondent', () => { | ||||
| @@ -257,7 +287,7 @@ describe('documents query params', () => { | ||||
|  | ||||
|   it('should show a list of documents filtered by no correspondent', () => { | ||||
|     cy.visit('/documents?sort=created&reverse=true&correspondent__isnull=1') | ||||
|     cy.contains('One document') | ||||
|     cy.contains('3 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by storage path', () => { | ||||
| @@ -267,7 +297,7 @@ describe('documents query params', () => { | ||||
|  | ||||
|   it('should show a list of documents filtered by no storage path', () => { | ||||
|     cy.visit('/documents?sort=created&reverse=true&storage_path__isnull=1') | ||||
|     cy.contains('3 documents') | ||||
|     cy.contains('5 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by title or content', () => { | ||||
| @@ -312,7 +342,7 @@ describe('documents query params', () => { | ||||
|     cy.visit( | ||||
|       '/documents?sort=created&reverse=true&created__date__gt=2022-03-23' | ||||
|     ) | ||||
|     cy.contains('3 documents') | ||||
|     cy.contains('5 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by created date less than', () => { | ||||
| @@ -324,7 +354,7 @@ describe('documents query params', () => { | ||||
|  | ||||
|   it('should show a list of documents filtered by added date greater than', () => { | ||||
|     cy.visit('/documents?sort=created&reverse=true&added__date__gt=2022-03-24') | ||||
|     cy.contains('2 documents') | ||||
|     cy.contains('4 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by added date less than', () => { | ||||
| @@ -338,4 +368,24 @@ describe('documents query params', () => { | ||||
|     ) | ||||
|     cy.contains('2 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by owner', () => { | ||||
|     cy.visit('/documents?owner__id=15') | ||||
|     cy.contains('One document') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by multiple owners', () => { | ||||
|     cy.visit('/documents?owner__id__in=6,15') | ||||
|     cy.contains('2 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by excluded owners', () => { | ||||
|     cy.visit('/documents?owner__id__none=6') | ||||
|     cy.contains('5 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by null owner', () => { | ||||
|     cy.visit('/documents?owner__isnull=true') | ||||
|     cy.contains('4 documents') | ||||
|   }) | ||||
| }) | ||||
|   | ||||
| @@ -143,6 +143,64 @@ | ||||
|                 } | ||||
|             }, | ||||
|             "notes": [] | ||||
|         }, | ||||
|         { | ||||
|             "id": 5, | ||||
|             "correspondent": null, | ||||
|             "document_type": null, | ||||
|             "storage_path": null, | ||||
|             "title": "Doc 5", | ||||
|             "content": "Test document 5", | ||||
|             "tags": [], | ||||
|             "created": "2023-05-01T07:24:18Z", | ||||
|             "created_date": "2023-05-02", | ||||
|             "modified": "2023-05-02T07:24:23.264859Z", | ||||
|             "added": "2023-05-02T07:24:22.922631Z", | ||||
|             "archive_serial_number": null, | ||||
|             "original_file_name": "doc5.pdf", | ||||
|             "archived_file_name": "doc5.pdf", | ||||
|             "owner": 15, | ||||
|             "user_can_change": true, | ||||
|             "permissions": { | ||||
|                 "view": { | ||||
|                     "users": [1], | ||||
|                     "groups": [] | ||||
|                 }, | ||||
|                 "change": { | ||||
|                     "users": [], | ||||
|                     "groups": [] | ||||
|                 } | ||||
|             }, | ||||
|             "notes": [] | ||||
|         }, | ||||
|         { | ||||
|             "id": 6, | ||||
|             "correspondent": null, | ||||
|             "document_type": null, | ||||
|             "storage_path": null, | ||||
|             "title": "Doc 6", | ||||
|             "content": "Test document 6", | ||||
|             "tags": [], | ||||
|             "created": "2023-05-01T10:24:18Z", | ||||
|             "created_date": "2023-05-02", | ||||
|             "modified": "2023-05-02T10:24:23.264859Z", | ||||
|             "added": "2023-05-02T10:24:22.922631Z", | ||||
|             "archive_serial_number": null, | ||||
|             "original_file_name": "doc6.pdf", | ||||
|             "archived_file_name": "doc6.pdf", | ||||
|             "owner": 6, | ||||
|             "user_can_change": true, | ||||
|             "permissions": { | ||||
|                 "view": { | ||||
|                     "users": [1], | ||||
|                     "groups": [] | ||||
|                 }, | ||||
|                 "change": { | ||||
|                     "users": [], | ||||
|                     "groups": [] | ||||
|                 } | ||||
|             }, | ||||
|             "notes": [] | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|   | ||||
| @@ -88,6 +88,10 @@ import { PermissionsUserComponent } from './components/common/input/permissions/ | ||||
| import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component' | ||||
| import { IfOwnerDirective } from './directives/if-owner.directive' | ||||
| import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive' | ||||
| import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component' | ||||
| import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component' | ||||
| import { PermissionsFilterDropdownComponent } from './components/common/permissions-filter-dropdown/permissions-filter-dropdown.component' | ||||
| import { UsernamePipe } from './pipes/username.pipe' | ||||
|  | ||||
| import localeAr from '@angular/common/locales/ar' | ||||
| import localeBe from '@angular/common/locales/be' | ||||
| @@ -111,8 +115,6 @@ 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 { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component' | ||||
| import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component' | ||||
|  | ||||
| registerLocaleData(localeAr) | ||||
| registerLocaleData(localeBe) | ||||
| @@ -213,6 +215,8 @@ function initializeApp(settings: SettingsService) { | ||||
|     IfObjectPermissionsDirective, | ||||
|     PermissionsDialogComponent, | ||||
|     PermissionsFormComponent, | ||||
|     PermissionsFilterDropdownComponent, | ||||
|     UsernamePipe, | ||||
|   ], | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
| @@ -253,6 +257,7 @@ function initializeApp(settings: SettingsService) { | ||||
|     PermissionsGuard, | ||||
|     DirtyDocGuard, | ||||
|     DirtySavedViewGuard, | ||||
|     UsernamePipe, | ||||
|   ], | ||||
|   bootstrap: [AppComponent], | ||||
| }) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
|   <div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> | ||||
|     <div class="list-group list-group-flush"> | ||||
|         <button *ngFor="let rd of relativeDates" class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.date)"> | ||||
|           <div _ngcontent-hga-c166="" class="selected-icon me-1"> | ||||
|           <div class="selected-icon me-1"> | ||||
|             <svg *ngIf="relativeDate === rd.date" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16"> | ||||
|               <path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/> | ||||
|             </svg> | ||||
|   | ||||
| @@ -0,0 +1,81 @@ | ||||
| <div class="btn-group w-100" ngbDropdown role="group"> | ||||
|     <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="isActive ? 'btn-primary' : 'btn-outline-primary'"> | ||||
|         <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|            <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" /> | ||||
|         </svg> {{title}} | ||||
|       <app-clearable-badge [selected]="isActive" (cleared)="reset()"></app-clearable-badge><span class="visually-hidden">selected</span> | ||||
|     </button> | ||||
|     <div class="dropdown-menu permission-filter-dropdown shadow py-0 w-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> | ||||
|         <div class="list-group list-group-flush"> | ||||
|             <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NONE)" [disabled]="disabled"> | ||||
|                 <div class="selected-icon me-1"> | ||||
|                     <svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.NONE" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16"> | ||||
|                         <path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/> | ||||
|                     </svg> | ||||
|                 </div> | ||||
|                 <div class="me-1"> | ||||
|                     <small i18n>All</small> | ||||
|                 </div> | ||||
|             </button> | ||||
|             <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SELF)" [disabled]="disabled"> | ||||
|                 <div class="selected-icon me-1"> | ||||
|                     <svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.SELF" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16"> | ||||
|                         <path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/> | ||||
|                     </svg> | ||||
|                 </div> | ||||
|                 <div class="me-1"> | ||||
|                     <small i18n>My documents</small> | ||||
|                 </div> | ||||
|             </button> | ||||
|             <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NOT_SELF)" [disabled]="disabled"> | ||||
|                 <div class="selected-icon me-1"> | ||||
|                     <svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.NOT_SELF" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16"> | ||||
|                         <path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/> | ||||
|                     </svg> | ||||
|                 </div> | ||||
|                 <div class="me-1"> | ||||
|                     <small i18n>Shared with me</small> | ||||
|                 </div> | ||||
|             </button> | ||||
|             <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled"> | ||||
|                 <div class="selected-icon me-1"> | ||||
|                     <svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.UNOWNED" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16"> | ||||
|                         <path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/> | ||||
|                     </svg> | ||||
|                 </div> | ||||
|                 <div class="me-1"> | ||||
|                     <small i18n>Unowned</small> | ||||
|                 </div> | ||||
|             </button> | ||||
|             <button *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" [disabled]="disabled"> | ||||
|                 <div class="selected-icon me-1"> | ||||
|                     <svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.OTHERS" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16"> | ||||
|                         <path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/> | ||||
|                     </svg> | ||||
|                 </div> | ||||
|                 <div class="me-1 w-100"> | ||||
|                     <ng-select | ||||
|                         name="user" | ||||
|                         class="user-select small" | ||||
|                         [(ngModel)]="selectionModel.includeUsers" | ||||
|                         [disabled]="disabled" | ||||
|                         [clearable]="false" | ||||
|                         [items]="users" | ||||
|                         bindLabel="username" | ||||
|                         multiple="true" | ||||
|                         bindValue="id" | ||||
|                         placeholder="Users" | ||||
|                         i18n-placeholder | ||||
|                         (change)="onUserSelect()"> | ||||
|                     </ng-select> | ||||
|                 </div> | ||||
|             </button> | ||||
|             <div *ngIf="selectionModel.ownerFilter === OwnerFilterType.NONE || selectionModel.ownerFilter === OwnerFilterType.NOT_SELF" class="list-group-item list-group-item-action d-flex align-items-center p-2 ps-3 border-bottom-0 border-start-0 border-end-0"> | ||||
|                 <div class="form-check form-switch"> | ||||
|                   <input type="checkbox" class="form-check-input" id="hideUnowned" [(ngModel)]="this.selectionModel.hideUnowned" (change)="onChange()" [disabled]="disabled"> | ||||
|                   <label class="form-check-label" for="hideUnowned"><small i18n>Hide unowned</small></label> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|   </div> | ||||
| @@ -0,0 +1,8 @@ | ||||
| .user-select { | ||||
|   min-width: 15rem; | ||||
| } | ||||
|  | ||||
| .selected-icon { | ||||
|     min-width: 1em; | ||||
|     min-height: 1em; | ||||
| } | ||||
| @@ -0,0 +1,132 @@ | ||||
| import { Component, EventEmitter, Input, Output } from '@angular/core' | ||||
| import { first } from 'rxjs' | ||||
| import { PaperlessUser } from 'src/app/data/paperless-user' | ||||
| import { | ||||
|   PermissionAction, | ||||
|   PermissionType, | ||||
|   PermissionsService, | ||||
| } from 'src/app/services/permissions.service' | ||||
| import { UserService } from 'src/app/services/rest/user.service' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
|  | ||||
| export class PermissionsSelectionModel { | ||||
|   ownerFilter: OwnerFilterType | ||||
|   hideUnowned: boolean | ||||
|   userID: number | ||||
|   includeUsers: number[] | ||||
|   excludeUsers: number[] | ||||
|  | ||||
|   clear() { | ||||
|     this.ownerFilter = OwnerFilterType.NONE | ||||
|     this.userID = null | ||||
|     this.hideUnowned = false | ||||
|     this.includeUsers = [] | ||||
|     this.excludeUsers = [] | ||||
|   } | ||||
| } | ||||
|  | ||||
| export enum OwnerFilterType { | ||||
|   NONE = 0, | ||||
|   SELF = 1, | ||||
|   NOT_SELF = 2, | ||||
|   OTHERS = 3, | ||||
|   UNOWNED = 4, | ||||
| } | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-permissions-filter-dropdown', | ||||
|   templateUrl: './permissions-filter-dropdown.component.html', | ||||
|   styleUrls: ['./permissions-filter-dropdown.component.scss'], | ||||
| }) | ||||
| export class PermissionsFilterDropdownComponent { | ||||
|   public PermissionAction = PermissionAction | ||||
|   public PermissionType = PermissionType | ||||
|   public OwnerFilterType = OwnerFilterType | ||||
|  | ||||
|   @Input() | ||||
|   title: string | ||||
|  | ||||
|   @Input() | ||||
|   disabled = false | ||||
|  | ||||
|   @Input() | ||||
|   selectionModel: PermissionsSelectionModel | ||||
|  | ||||
|   @Output() | ||||
|   ownerFilterSet = new EventEmitter<PermissionsSelectionModel>() | ||||
|  | ||||
|   users: PaperlessUser[] | ||||
|  | ||||
|   hideUnowned: boolean | ||||
|  | ||||
|   get isActive(): boolean { | ||||
|     return ( | ||||
|       this.selectionModel.ownerFilter !== OwnerFilterType.NONE || | ||||
|       this.selectionModel.hideUnowned | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   constructor( | ||||
|     permissionsService: PermissionsService, | ||||
|     userService: UserService, | ||||
|     private settingsService: SettingsService | ||||
|   ) { | ||||
|     if ( | ||||
|       permissionsService.currentUserCan( | ||||
|         PermissionAction.View, | ||||
|         PermissionType.User | ||||
|       ) | ||||
|     ) { | ||||
|       userService | ||||
|         .listAll() | ||||
|         .pipe(first()) | ||||
|         .subscribe({ | ||||
|           next: (result) => (this.users = result.results), | ||||
|         }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   reset() { | ||||
|     this.selectionModel.clear() | ||||
|     this.onChange() | ||||
|   } | ||||
|  | ||||
|   setFilter(type: OwnerFilterType) { | ||||
|     this.selectionModel.ownerFilter = type | ||||
|     if (this.selectionModel.ownerFilter === OwnerFilterType.SELF) { | ||||
|       this.selectionModel.includeUsers = [] | ||||
|       this.selectionModel.excludeUsers = [] | ||||
|       this.selectionModel.userID = this.settingsService.currentUser.id | ||||
|       this.selectionModel.hideUnowned = false | ||||
|     } else if (this.selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) { | ||||
|       this.selectionModel.userID = null | ||||
|       this.selectionModel.includeUsers = [] | ||||
|       this.selectionModel.excludeUsers = [this.settingsService.currentUser.id] | ||||
|       this.selectionModel.hideUnowned = false | ||||
|     } else if (this.selectionModel.ownerFilter === OwnerFilterType.NONE) { | ||||
|       this.selectionModel.userID = null | ||||
|       this.selectionModel.includeUsers = [] | ||||
|       this.selectionModel.excludeUsers = [] | ||||
|       this.selectionModel.hideUnowned = false | ||||
|     } else if (this.selectionModel.ownerFilter === OwnerFilterType.UNOWNED) { | ||||
|       this.selectionModel.userID = null | ||||
|       this.selectionModel.includeUsers = [] | ||||
|       this.selectionModel.excludeUsers = [] | ||||
|       this.selectionModel.hideUnowned = false | ||||
|     } | ||||
|     this.onChange() | ||||
|   } | ||||
|  | ||||
|   onChange() { | ||||
|     this.ownerFilterSet.emit(this.selectionModel) | ||||
|   } | ||||
|  | ||||
|   onUserSelect() { | ||||
|     if (this.selectionModel.includeUsers?.length) { | ||||
|       this.selectionModel.ownerFilter = OwnerFilterType.OTHERS | ||||
|     } else { | ||||
|       this.selectionModel.ownerFilter = OwnerFilterType.NONE | ||||
|     } | ||||
|     this.onChange() | ||||
|   } | ||||
| } | ||||
| @@ -106,6 +106,12 @@ | ||||
|               </svg> | ||||
|               <small>{{document.created_date | customDate:'mediumDate'}}</small> | ||||
|             </div> | ||||
|             <div *ngIf="document.owner && document.owner !== settingsService.currentUser.id" class="list-group-item bg-light text-dark p-1 border-0"> | ||||
|               <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/> | ||||
|               </svg> | ||||
|               <small>{{document.owner | username}}</small> | ||||
|             </div> | ||||
|             <div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score"> | ||||
|               <small class="text-muted" i18n>Score:</small> | ||||
|               <ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar> | ||||
|   | ||||
| @@ -23,7 +23,7 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission | ||||
| export class DocumentCardLargeComponent extends ComponentWithPermissions { | ||||
|   constructor( | ||||
|     private documentService: DocumentService, | ||||
|     private settingsService: SettingsService | ||||
|     public settingsService: SettingsService | ||||
|   ) { | ||||
|     super() | ||||
|   } | ||||
|   | ||||
| @@ -38,15 +38,15 @@ | ||||
|       <div class="list-group list-group-flush border-0 pt-1 pb-2 card-info"> | ||||
|         <button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title | ||||
|          (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> | ||||
|           <svg class="metadata-icon me-2 text-muted bi bi-file-earmark" viewBox="0 0 16 16" fill="currentColor"> | ||||
|             <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/> | ||||
|           <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#file-earmark"/> | ||||
|           </svg> | ||||
|           <small>{{(document.document_type$ | async)?.name}}</small> | ||||
|         </button> | ||||
|         <button *ngIf="document.storage_path" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title | ||||
|          (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()"> | ||||
|           <svg class="metadata-icon me-2 text-muted bi bi-folder" viewBox="0 0 16 16" fill="currentColor"> | ||||
|             <path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/> | ||||
|           <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#folder"/> | ||||
|           </svg> | ||||
|           <small>{{(document.storage_path$ | async)?.name}}</small> | ||||
|         </button> | ||||
| @@ -59,18 +59,23 @@ | ||||
|             </div> | ||||
|           </ng-template> | ||||
|           <div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip"> | ||||
|             <svg class="metadata-icon me-2 text-muted bi bi-calendar-event" viewBox="0 0 16 16" fill="currentColor"> | ||||
|               <path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/> | ||||
|               <path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/> | ||||
|             <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#calendar-event"/> | ||||
|             </svg> | ||||
|             <small>{{document.created_date | customDate:'mediumDate'}}</small> | ||||
|           </div> | ||||
|           <div *ngIf="document.archive_serial_number" class="ps-0 p-1"> | ||||
|             <svg class="metadata-icon me-2 text-muted bi bi-upc-scan" viewBox="0 0 16 16" fill="currentColor"> | ||||
|               <path d="M1.5 1a.5.5 0 0 0-.5.5v3a.5.5 0 0 1-1 0v-3A1.5 1.5 0 0 1 1.5 0h3a.5.5 0 0 1 0 1h-3zM11 .5a.5.5 0 0 1 .5-.5h3A1.5 1.5 0 0 1 16 1.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 1-.5-.5zM.5 11a.5.5 0 0 1 .5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 1 0 1h-3A1.5 1.5 0 0 1 0 14.5v-3a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v3a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 1 .5-.5zM3 4.5a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-7zm3 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7z"/> | ||||
|             </svg> | ||||
|             <small>#{{document.archive_serial_number}}</small> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div *ngIf="document.archive_serial_number" class="ps-0 p-1"> | ||||
|           <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#upc-scan"/> | ||||
|           </svg> | ||||
|           <small>#{{document.archive_serial_number}}</small> | ||||
|         </div> | ||||
|         <div *ngIf="document.owner && document.owner !== settingsService.currentUser.id" class="ps-0 p-1"> | ||||
|           <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/> | ||||
|           </svg> | ||||
|           <small>{{document.owner | username}}</small> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="d-flex justify-content-between align-items-center"> | ||||
|   | ||||
| @@ -24,7 +24,7 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission | ||||
| export class DocumentCardSmallComponent extends ComponentWithPermissions { | ||||
|   constructor( | ||||
|     private documentService: DocumentService, | ||||
|     private settingsService: SettingsService | ||||
|     public settingsService: SettingsService | ||||
|   ) { | ||||
|     super() | ||||
|   } | ||||
|   | ||||
| @@ -142,6 +142,13 @@ | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>Title</th> | ||||
|       <th class="d-none d-xl-table-cell" | ||||
|         appSortable="owner" | ||||
|         title="Sort by owner" i18n-title | ||||
|         [currentSortField]="list.sortField" | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>Owner</th> | ||||
|       <th *ngIf="notesEnabled" class="d-none d-xl-table-cell" | ||||
|         appSortable="num_notes" | ||||
|         title="Sort by notes" i18n-title | ||||
| @@ -198,6 +205,9 @@ | ||||
|           <a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> | ||||
|           <app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></app-tag> | ||||
|         </td> | ||||
|         <td> | ||||
|           {{d.owner | username}} | ||||
|         </td> | ||||
|         <td *ngIf="notesEnabled" class="d-none d-xl-table-cell"> | ||||
|           <a *ngIf="d.notes.length" routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0"> | ||||
|             <span class="badge rounded-pill bg-light border text-primary"> | ||||
|   | ||||
| @@ -58,20 +58,26 @@ | ||||
|             [documentCounts]="storagePathDocumentCounts" | ||||
|             [allowSelectNone]="true"></app-filterable-dropdown> | ||||
|         </div> | ||||
|         <div class="d-flex flex-wrap"> | ||||
|         <div class="d-flex flex-wrap mb-2 mb-xxl-0"> | ||||
|           <app-date-dropdown class="mb-2 mb-xl-0" | ||||
|             title="Created" i18n-title | ||||
|             (datesSet)="updateRules()" | ||||
|             [(dateBefore)]="dateCreatedBefore" | ||||
|             [(dateAfter)]="dateCreatedAfter" | ||||
|             [(relativeDate)]="dateCreatedRelativeDate"></app-date-dropdown> | ||||
|           <app-date-dropdown class="mb-2 mb-xl-0" | ||||
|           <app-date-dropdown class="mb-2 mb-xl-0 me-2" | ||||
|             title="Added" i18n-title | ||||
|             (datesSet)="updateRules()" | ||||
|             [(dateBefore)]="dateAddedBefore" | ||||
|             [(dateAfter)]="dateAddedAfter" | ||||
|             [(relativeDate)]="dateAddedRelativeDate"></app-date-dropdown> | ||||
|         </div> | ||||
|         <div class="d-flex flex-wrap"> | ||||
|           <app-permissions-filter-dropdown class="mb-2 mb-xl-0" | ||||
|             title="Permissions" i18n-title | ||||
|             (ownerFilterSet)="updateRules()" | ||||
|             [(selectionModel)]="permissionsSelectionModel"></app-permissions-filter-dropdown> | ||||
|         </div> | ||||
|      </div> | ||||
|    </div> | ||||
|    <div class="w-100 d-xxl-none"></div> | ||||
|   | ||||
| @@ -43,6 +43,10 @@ import { | ||||
|   FILTER_DOCUMENT_TYPE, | ||||
|   FILTER_CORRESPONDENT, | ||||
|   FILTER_STORAGE_PATH, | ||||
|   FILTER_OWNER, | ||||
|   FILTER_OWNER_DOES_NOT_INCLUDE, | ||||
|   FILTER_OWNER_ISNULL, | ||||
|   FILTER_OWNER_ANY, | ||||
| } from 'src/app/data/filter-rule-type' | ||||
| import { | ||||
|   FilterableDropdownSelectionModel, | ||||
| @@ -59,6 +63,11 @@ import { PaperlessDocument } from 'src/app/data/paperless-document' | ||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||
| import { RelativeDate } from '../../common/date-dropdown/date-dropdown.component' | ||||
| import { | ||||
|   OwnerFilterType, | ||||
|   PermissionsSelectionModel, | ||||
| } from '../../common/permissions-filter-dropdown/permissions-filter-dropdown.component' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
|  | ||||
| const TEXT_FILTER_TARGET_TITLE = 'title' | ||||
| const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content' | ||||
| @@ -136,6 +145,15 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|  | ||||
|         case FILTER_ASN: | ||||
|           return $localize`ASN: ${rule.value}` | ||||
|  | ||||
|         case FILTER_OWNER: | ||||
|           return $localize`Owner: ${rule.value}` | ||||
|  | ||||
|         case FILTER_OWNER_DOES_NOT_INCLUDE: | ||||
|           return $localize`Owner not in: ${rule.value}` | ||||
|  | ||||
|         case FILTER_OWNER_ISNULL: | ||||
|           return $localize`Without an owner` | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -147,7 +165,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     private tagService: TagService, | ||||
|     private correspondentService: CorrespondentService, | ||||
|     private documentService: DocumentService, | ||||
|     private storagePathService: StoragePathService | ||||
|     private storagePathService: StoragePathService, | ||||
|     private settingsService: SettingsService | ||||
|   ) {} | ||||
|  | ||||
|   @ViewChild('textFilterInput') | ||||
| @@ -241,6 +260,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|   dateCreatedRelativeDate: RelativeDate | ||||
|   dateAddedRelativeDate: RelativeDate | ||||
|  | ||||
|   permissionsSelectionModel = new PermissionsSelectionModel() | ||||
|  | ||||
|   _unmodifiedFilterRules: FilterRule[] = [] | ||||
|   _filterRules: FilterRule[] = [] | ||||
|  | ||||
| @@ -274,6 +295,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     this.dateCreatedRelativeDate = null | ||||
|     this.dateAddedRelativeDate = null | ||||
|     this.textFilterModifier = TEXT_FILTER_MODIFIER_EQUALS | ||||
|     this.permissionsSelectionModel.clear() | ||||
|  | ||||
|     value.forEach((rule) => { | ||||
|       switch (rule.rule_type) { | ||||
| @@ -441,6 +463,35 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|           this.textFilterModifier = TEXT_FILTER_MODIFIER_LT | ||||
|           this._textFilter = rule.value | ||||
|           break | ||||
|         case FILTER_OWNER: | ||||
|           this.permissionsSelectionModel.ownerFilter = OwnerFilterType.SELF | ||||
|           this.permissionsSelectionModel.hideUnowned = false | ||||
|           if (rule.value) | ||||
|             this.permissionsSelectionModel.userID = parseInt(rule.value, 10) | ||||
|           break | ||||
|         case FILTER_OWNER_ANY: | ||||
|           this.permissionsSelectionModel.ownerFilter = OwnerFilterType.OTHERS | ||||
|           if (rule.value) | ||||
|             this.permissionsSelectionModel.includeUsers.push( | ||||
|               parseInt(rule.value, 10) | ||||
|             ) | ||||
|           break | ||||
|         case FILTER_OWNER_DOES_NOT_INCLUDE: | ||||
|           this.permissionsSelectionModel.ownerFilter = OwnerFilterType.NOT_SELF | ||||
|           if (rule.value) | ||||
|             this.permissionsSelectionModel.excludeUsers.push( | ||||
|               parseInt(rule.value, 10) | ||||
|             ) | ||||
|           break | ||||
|         case FILTER_OWNER_ISNULL: | ||||
|           if (rule.value === 'true' || rule.value === '1') { | ||||
|             this.permissionsSelectionModel.hideUnowned = false | ||||
|             this.permissionsSelectionModel.ownerFilter = OwnerFilterType.UNOWNED | ||||
|           } else { | ||||
|             this.permissionsSelectionModel.hideUnowned = | ||||
|               rule.value === 'false' || rule.value === '0' | ||||
|             break | ||||
|           } | ||||
|       } | ||||
|     }) | ||||
|     this.rulesModified = filterRulesDiffer( | ||||
| @@ -702,6 +753,40 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (this.permissionsSelectionModel.ownerFilter == OwnerFilterType.SELF) { | ||||
|       filterRules.push({ | ||||
|         rule_type: FILTER_OWNER, | ||||
|         value: this.permissionsSelectionModel.userID.toString(), | ||||
|       }) | ||||
|     } else if ( | ||||
|       this.permissionsSelectionModel.ownerFilter == OwnerFilterType.NOT_SELF | ||||
|     ) { | ||||
|       filterRules.push({ | ||||
|         rule_type: FILTER_OWNER_DOES_NOT_INCLUDE, | ||||
|         value: this.permissionsSelectionModel.excludeUsers?.join(','), | ||||
|       }) | ||||
|     } else if ( | ||||
|       this.permissionsSelectionModel.ownerFilter == OwnerFilterType.OTHERS | ||||
|     ) { | ||||
|       filterRules.push({ | ||||
|         rule_type: FILTER_OWNER_ANY, | ||||
|         value: this.permissionsSelectionModel.includeUsers?.join(','), | ||||
|       }) | ||||
|     } else if ( | ||||
|       this.permissionsSelectionModel.ownerFilter == OwnerFilterType.UNOWNED | ||||
|     ) { | ||||
|       filterRules.push({ | ||||
|         rule_type: FILTER_OWNER_ISNULL, | ||||
|         value: 'true', | ||||
|       }) | ||||
|     } | ||||
|  | ||||
|     if (this.permissionsSelectionModel.hideUnowned) { | ||||
|       filterRules.push({ | ||||
|         rule_type: FILTER_OWNER_ISNULL, | ||||
|         value: 'false', | ||||
|       }) | ||||
|     } | ||||
|     return filterRules | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -41,6 +41,11 @@ export const FILTER_TITLE_CONTENT = 19 | ||||
| export const FILTER_FULLTEXT_QUERY = 20 | ||||
| export const FILTER_FULLTEXT_MORELIKE = 21 | ||||
|  | ||||
| export const FILTER_OWNER = 32 | ||||
| export const FILTER_OWNER_ANY = 33 | ||||
| export const FILTER_OWNER_ISNULL = 34 | ||||
| export const FILTER_OWNER_DOES_NOT_INCLUDE = 35 | ||||
|  | ||||
| export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|   { | ||||
|     id: FILTER_TITLE, | ||||
| @@ -242,6 +247,30 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|     datatype: 'number', | ||||
|     multi: false, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_OWNER, | ||||
|     filtervar: 'owner__id', | ||||
|     datatype: 'number', | ||||
|     multi: false, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_OWNER_ANY, | ||||
|     filtervar: 'owner__id__in', | ||||
|     datatype: 'number', | ||||
|     multi: true, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_OWNER_ISNULL, | ||||
|     filtervar: 'owner__isnull', | ||||
|     datatype: 'boolean', | ||||
|     multi: false, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_OWNER_DOES_NOT_INCLUDE, | ||||
|     filtervar: 'owner__id__none', | ||||
|     datatype: 'number', | ||||
|     multi: true, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| export interface FilterRuleType { | ||||
|   | ||||
							
								
								
									
										42
									
								
								src-ui/src/app/pipes/username.pipe.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src-ui/src/app/pipes/username.pipe.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import { Pipe, PipeTransform } from '@angular/core' | ||||
| import { UserService } from '../services/rest/user.service' | ||||
| import { | ||||
|   PermissionAction, | ||||
|   PermissionType, | ||||
|   PermissionsService, | ||||
| } from '../services/permissions.service' | ||||
| import { PaperlessUser } from '../data/paperless-user' | ||||
|  | ||||
| @Pipe({ | ||||
|   name: 'username', | ||||
| }) | ||||
| export class UsernamePipe implements PipeTransform { | ||||
|   users: PaperlessUser[] | ||||
|  | ||||
|   constructor( | ||||
|     permissionsService: PermissionsService, | ||||
|     userService: UserService | ||||
|   ) { | ||||
|     if ( | ||||
|       permissionsService.currentUserCan( | ||||
|         PermissionAction.View, | ||||
|         PermissionType.User | ||||
|       ) | ||||
|     ) { | ||||
|       userService.listAll().subscribe((r) => (this.users = r.results)) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   transform(userID: number): string { | ||||
|     return this.users | ||||
|       ? this.getName(this.users.find((u) => u.id === userID)) ?? '' | ||||
|       : $localize`Shared` | ||||
|   } | ||||
|  | ||||
|   getName(user: PaperlessUser): string { | ||||
|     if (!user) return '' | ||||
|     const name = [user.first_name, user.last_name].join(' ') | ||||
|     if (name.length > 1) return name.trim() | ||||
|     return user.username | ||||
|   } | ||||
| } | ||||
| @@ -23,6 +23,7 @@ export const DOCUMENT_SORT_FIELDS = [ | ||||
|   { field: 'added', name: $localize`Added` }, | ||||
|   { field: 'modified', name: $localize`Modified` }, | ||||
|   { field: 'num_notes', name: $localize`Notes` }, | ||||
|   { field: 'owner', name: $localize`Owner` }, | ||||
| ] | ||||
|  | ||||
| export const DOCUMENT_SORT_FIELDS_FULLTEXT = [ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon