Merge pull request #3309 from paperless-ngx/feature-owner-filtering

Feature: owner filtering
This commit is contained in:
shamoon
2023-05-11 10:05:51 -07:00
committed by GitHub
33 changed files with 1018 additions and 179 deletions

View File

@@ -18,8 +18,8 @@
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)" (selectItem)="itemSelected($event)" i18n-placeholder>
<button type="button" *ngIf="!searchFieldEmpty" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
<svg fill="currentColor" class="buttonicon-sm me-1">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
</button>
</form>
@@ -107,7 +107,7 @@
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg><span>&nbsp;{{d.title | documentTitle}}</span>
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
<svg fill="currentColor" class="toolbaricon">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
</span>

View File

@@ -6,10 +6,10 @@
<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">
<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>
<div class="selected-icon me-1">
<svg *ngIf="relativeDate === rd.date" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
{{rd.name}}
</button>
@@ -18,8 +18,8 @@
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div i18n>After</div>
<a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()">
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<small i18n>Clear</small>
</a>
@@ -29,8 +29,8 @@
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
<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 fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
</svg>
</button>
</div>
@@ -41,8 +41,8 @@
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div i18n>Before</div>
<a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()">
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<small i18n>Clear</small>
</a>
@@ -52,8 +52,8 @@
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
<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 fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
</svg>
</button>
</div>

View File

@@ -1,18 +1,18 @@
<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)="toggleItem($event)" [disabled]="disabled">
<div class="selected-icon me-1">
<ng-container *ngIf="isChecked()">
<svg 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 fill="currentColor" class="buttonicon-sm bi-check">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</ng-container>
<ng-container *ngIf="isPartiallyChecked()">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-dash" viewBox="0 0 16 16">
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/>
<svg fill="currentColor" class="buttonicon-sm bi-dash">
<use xlink:href="assets/bootstrap-icons.svg#dash"/>
</svg>
</ng-container>
<ng-container *ngIf="isExcluded()">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
<svg fill="currentColor" class="buttonicon-sm bi-x">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
</ng-container>
</div>

View File

@@ -0,0 +1,82 @@
<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 class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
</svg>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div>
<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" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</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" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</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" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</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" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</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" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</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 w-100">
<input type="checkbox" class="form-check-input" id="hideUnowned" [(ngModel)]="this.selectionModel.hideUnowned" (change)="onChange()" [disabled]="disabled">
<label class="form-check-label w-100" for="hideUnowned"><small i18n>Hide unowned</small></label>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
.user-select {
min-width: 15rem;
}
.selected-icon {
min-width: 1em;
min-height: 1em;
}

View File

@@ -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()
}
}

View File

@@ -1,13 +1,13 @@
<div class="row">
<div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select">
<div class="d-flex flex-wrap gap-4">
<div class="d-flex align-items-center" role="group" aria-label="Select">
<button class="btn btn-sm btn-outline-secondary" (click)="list.selectNone()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#slash-circle" />
</svg>&nbsp;<ng-container i18n>Cancel</ng-container>
</button>
</div>
<div class="col-auto mb-2 mb-xl-0 ms-auto ms-md-0" role="group" aria-label="Select">
<label class="me-2 mb-0" i18n>Select:</label>
<div class="d-flex align-items-center gap-2" role="group" aria-label="Select">
<label class="me-2" i18n>Select:</label>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
@@ -21,11 +21,9 @@
</button>
</div>
</div>
<div class="w-100 d-xl-none"></div>
<div class="col-auto mb-2 mb-xl-0">
<div class="d-flex" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<label class="ms-auto mt-1 mb-0 me-2" i18n>Edit:</label>
<app-filterable-dropdown class="me-2 me-md-3" title="Tags" icon="tag-fill" i18n-title
<div class="d-flex align-items-center gap-2" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<label class="me-2" i18n>Edit:</label>
<app-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder
[items]="tags"
[disabled]="!userCanEditAll"
@@ -37,7 +35,7 @@
[documentCounts]="tagDocumentCounts"
(apply)="setTags($event)">
</app-filterable-dropdown>
<app-filterable-dropdown class="me-2 me-md-3" title="Correspondent" icon="person-fill" i18n-title
<app-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[items]="correspondents"
[disabled]="!userCanEditAll"
@@ -48,7 +46,7 @@
[documentCounts]="correspondentDocumentCounts"
(apply)="setCorrespondents($event)">
</app-filterable-dropdown>
<app-filterable-dropdown class="me-2 me-md-3" title="Document type" icon="file-earmark-fill" i18n-title
<app-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder
[items]="documentTypes"
[disabled]="!userCanEditAll"
@@ -59,7 +57,7 @@
[documentCounts]="documentTypeDocumentCounts"
(apply)="setDocumentTypes($event)">
</app-filterable-dropdown>
<app-filterable-dropdown class="me-2 me-md-3" title="Storage path" icon="folder-fill" i18n-title
<app-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[items]="storagePaths"
[disabled]="!userCanEditAll"
@@ -70,18 +68,17 @@
[documentCounts]="storagePathDocumentCounts"
(apply)="setStoragePaths($event)">
</app-filterable-dropdown>
</div>
</div>
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
<div class="btn-toolbar me-2">
<div class="d-flex align-items-center gap-2 ms-auto">
<div class="btn-toolbar">
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
</svg>&nbsp;<ng-container i18n>Permissions</ng-container>
</svg><div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Permissions</ng-container></div>
</button>
<div ngbDropdown class="me-2 d-flex">
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
@@ -94,7 +91,7 @@
</div>
</div>
<div class="btn-group btn-group-sm me-2">
<div class="btn-group btn-group-sm">
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
<svg *ngIf="!awaitingDownload" class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-down" />
@@ -134,7 +131,7 @@
</div>
</div>
<div class="btn-group btn-group-sm me-2">
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />

View File

@@ -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>

View File

@@ -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()
}

View File

@@ -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">

View File

@@ -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()
}

View File

@@ -81,15 +81,15 @@
</app-page-header>
<div class="row sticky-top pt-3 pt-sm-4 pb-2 pb-lg-4 bg-body">
<div class="row sticky-top pt-3 pt-sm-4 pb-3 pb-lg-4 bg-body">
<app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" [selectionData]="list.selectionData" #filterEditor></app-filter-editor>
<app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
</div>
<ng-template #pagination>
<div class="d-flex justify-content-between align-items-center">
<p>
<div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<ng-container *ngIf="list.isReloading">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
@@ -98,9 +98,14 @@
<ng-container *ngIf="!list.isReloading">
<span i18n *ngIf="list.selected.size === 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>&nbsp;<span i18n *ngIf="isFiltered">(filtered)</span>
</ng-container>
</p>
<button *ngIf="!list.isReloading && isFiltered" class="btn btn-link py-0" (click)="resetFilters()">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg><small i18n>Reset filters</small>
</button>
</div>
<ngb-pagination *ngIf="list.collectionSize" [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" aria-label="Default pagination"></ngb-pagination>
[rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination>
</div>
</ng-template>
@@ -142,6 +147,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 +210,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">

View File

@@ -300,4 +300,8 @@ export class DocumentListComponent
get notesEnabled(): boolean {
return this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)
}
resetFilters() {
this.filterEditor.resetSelected()
}
}

View File

@@ -1,5 +1,5 @@
<div class="row flex-wrap" tourAnchor="tour.documents-filter-editor">
<div class="col mb-2 mb-xxl-0">
<div class="col mb-3 mb-xxl-0">
<div class="form-inline d-flex align-items-center">
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
<div ngbDropdown>
@@ -12,8 +12,8 @@
<option *ngFor="let m of textFilterModifiers" ngbDropdownItem [value]="m.id">{{m.label}}</option>
</select>
<button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
<svg fill="currentColor" class="buttonicon-sm me-1">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
</button>
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget === 'fulltext-morelike'">
@@ -22,8 +22,8 @@
</div>
<div class="w-100 d-xxl-none"></div>
<div class="col col-xl-auto">
<div class="d-flex flex-wrap">
<div class="d-flex flex-wrap mb-2 mb-xxl-0">
<div class="d-flex flex-wrap gap-3">
<div class="d-flex flex-wrap gap-2">
<app-filterable-dropdown class="flex-fill" title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder
[items]="tags"
@@ -49,7 +49,7 @@
(opened)="onDocumentTypeDropdownOpen()"
[documentCounts]="documentTypeDocumentCounts"
[allowSelectNone]="true"></app-filterable-dropdown>
<app-filterable-dropdown class="me-2 flex-fill" title="Storage path" icon="folder-fill" i18n-title
<app-filterable-dropdown class="flex-fill" title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[items]="storagePaths"
[(selectionModel)]="storagePathSelectionModel"
@@ -58,28 +58,33 @@
[documentCounts]="storagePathDocumentCounts"
[allowSelectNone]="true"></app-filterable-dropdown>
</div>
<div class="d-flex flex-wrap">
<app-date-dropdown class="mb-2 mb-xl-0"
<div class="d-flex flex-wrap gap-2">
<app-date-dropdown
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
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
title="Permissions" i18n-title
(ownerFilterSet)="updateRules()"
[(selectionModel)]="permissionsSelectionModel"></app-permissions-filter-dropdown>
</div>
<div class="d-flex flex-wrap d-none d-sm-inline-block">
<button class="btn btn-outline-secondary btn-sm" [disabled]="!rulesModified" (click)="resetSelected()">
<svg class="toolbaricon ms-n1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"></use>
</svg><ng-container i18n>Reset filters</ng-container>
</button>
</div>
</div>
</div>
<div class="w-100 d-xxl-none"></div>
<div class="col col-xl-auto ps-xxl-0">
<button class="btn btn-link btn-sm px-0" [disabled]="!rulesModified" (click)="resetSelected()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1 ms-n1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg><ng-container i18n>Reset filters</ng-container>
</button>
</div>
</div>

View File

@@ -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
}

View File

@@ -2,7 +2,7 @@
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *appIfPermissions="{ action: PermissionAction.Add, type: permissionType }" i18n>Create</button>
</app-page-header>
<div class="row">
<div class="row mb-3">
<div class="col-md mb-2 mb-xl-0">
<div class="form-inline d-flex align-items-center">
<label class="text-muted me-2 mb-0" i18n>Filter by:</label>
@@ -10,7 +10,7 @@
</div>
</div>
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination>
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
</div>
<table class="table table-striped align-middle border shadow-sm">
@@ -72,5 +72,5 @@
<div class="d-flex">
<div i18n *ngIf="collectionSize > 0">{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</div>
<ngb-pagination *ngIf="collectionSize > 20" class="ms-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination>
<ngb-pagination *ngIf="collectionSize > 20" class="ms-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
</div>

View File

@@ -125,8 +125,8 @@
</div>
<div class="col-2">
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
<svg fill="currentColor" class="buttonicon-sm me-1">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg><ng-container i18n>Reset</ng-container>
</button>
</div>