mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Merge pull request #2147 from paperless-ngx/feature-permissions
Feature: multi-user permissions
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
</svg>
|
||||
<span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span>
|
||||
</a>
|
||||
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1">
|
||||
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
|
||||
<svg width="1em" height="1em" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#search"/>
|
||||
@@ -39,7 +39,7 @@
|
||||
<p class="small mb-0 px-3 text-muted" i18n>Logged in as {{this.settingsService.displayName}}</p>
|
||||
<div class="dropdown-divider"></div>
|
||||
</div>
|
||||
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()">
|
||||
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
|
||||
<svg class="sidebaricon me-2" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||
</svg><ng-container i18n>Settings</ng-container>
|
||||
@@ -72,7 +72,7 @@
|
||||
</svg><span> <ng-container i18n>Dashboard</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
||||
@@ -80,79 +80,82 @@
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews.length > 0'>
|
||||
<span i18n>Saved views</span>
|
||||
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
||||
</svg><span> {{view.name}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews.length > 0'>
|
||||
<span i18n>Saved views</span>
|
||||
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
||||
</svg><span> {{view.name}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
|
||||
<span i18n>Open documents</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||
</svg><span> {{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">
|
||||
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
|
||||
<span i18n>Open documents</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||
</svg><span> {{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">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item w-100" *ngIf="openDocuments.length >= 1">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item w-100" *ngIf="openDocuments.length >= 1">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg><span> <ng-container i18n>Close all</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</svg><span> <ng-container i18n>Close all</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
|
||||
<span i18n>Manage</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person"/>
|
||||
</svg><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" tourAnchor="tour.tags">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }" tourAnchor="tour.tags">
|
||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
|
||||
</svg><span> <ng-container i18n>Tags</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Document types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
|
||||
</svg><span> <ng-container i18n>Document types</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Storage paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
|
||||
</svg><span> <ng-container i18n>Storage paths</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" tourAnchor="tour.file-tasks">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }" tourAnchor="tour.file-tasks">
|
||||
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()" ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<span *ngIf="tasksService.failedFileTasks.length > 0 && slimSidebarEnabled" class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
@@ -160,14 +163,14 @@
|
||||
</svg><span> <ng-container i18n>File Tasks<span *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span></ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
|
||||
</svg><span> <ng-container i18n>Logs</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" tourAnchor="tour.settings">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }" tourAnchor="tour.settings">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||
|
@@ -26,13 +26,17 @@ import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-app-frame',
|
||||
templateUrl: './app-frame.component.html',
|
||||
styleUrls: ['./app-frame.component.scss'],
|
||||
})
|
||||
export class AppFrameComponent implements OnInit, ComponentCanDeactivate {
|
||||
export class AppFrameComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, ComponentCanDeactivate
|
||||
{
|
||||
constructor(
|
||||
public router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
@@ -44,7 +48,9 @@ export class AppFrameComponent implements OnInit, ComponentCanDeactivate {
|
||||
public settingsService: SettingsService,
|
||||
public tasksService: TasksService,
|
||||
private readonly toastService: ToastService
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { interval, Subject, switchMap, take } from 'rxjs'
|
||||
import { interval, Subject, take } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirm-dialog',
|
||||
|
@@ -5,10 +5,16 @@
|
||||
</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 *appIfOwner="object">
|
||||
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
|
@@ -5,6 +5,7 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-correspondent-edit-dialog',
|
||||
@@ -12,8 +13,12 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
|
||||
styleUrls: ['./correspondent-edit-dialog.component.scss'],
|
||||
})
|
||||
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
||||
constructor(service: CorrespondentService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
constructor(
|
||||
service: CorrespondentService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal, userService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
@@ -30,6 +35,7 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
permissions_form: new FormControl(null),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -6,10 +6,16 @@
|
||||
</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"></app-input-check>
|
||||
<div class="col">
|
||||
<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"></app-input-check>
|
||||
</div>
|
||||
|
||||
<div *appIfOwner="object">
|
||||
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
@@ -5,6 +5,7 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-type-edit-dialog',
|
||||
@@ -12,8 +13,12 @@ import { DocumentTypeService } from 'src/app/services/rest/document-type.service
|
||||
styleUrls: ['./document-type-edit-dialog.component.scss'],
|
||||
})
|
||||
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
||||
constructor(service: DocumentTypeService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
constructor(
|
||||
service: DocumentTypeService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal, userService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
@@ -30,6 +35,7 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
permissions_form: new FormControl(null),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -4,17 +4,25 @@ 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 { PaperlessUser } from 'src/app/data/paperless-user'
|
||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { PermissionsFormObject } from '../input/permissions/permissions-form/permissions-form.component'
|
||||
|
||||
@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>,
|
||||
private activeModal: NgbActiveModal
|
||||
private activeModal: NgbActiveModal,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
users: PaperlessUser[]
|
||||
|
||||
@Input()
|
||||
dialogMode: string = 'create'
|
||||
|
||||
@@ -36,6 +44,14 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.object != null) {
|
||||
if (this.object['permissions']) {
|
||||
this.object['set_permissions'] = this.object['permissions']
|
||||
}
|
||||
|
||||
this.object['permissions_form'] = {
|
||||
owner: (this.object as ObjectWithPermissions).owner,
|
||||
set_permissions: (this.object as ObjectWithPermissions).permissions,
|
||||
}
|
||||
this.objectForm.patchValue(this.object)
|
||||
}
|
||||
|
||||
@@ -43,6 +59,8 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||
setTimeout(() => {
|
||||
this.closeEnabled = true
|
||||
})
|
||||
|
||||
this.userService.listAll().subscribe((r) => (this.users = r.results))
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
@@ -77,10 +95,16 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||
}
|
||||
|
||||
save() {
|
||||
var newObject = Object.assign(
|
||||
Object.assign({}, this.object),
|
||||
this.objectForm.value
|
||||
)
|
||||
const formValues = Object.assign({}, this.objectForm.value)
|
||||
const permissionsObject: PermissionsFormObject =
|
||||
this.objectForm.get('permissions_form')?.value
|
||||
if (permissionsObject) {
|
||||
formValues.owner = permissionsObject.owner
|
||||
formValues.set_permissions = permissionsObject.set_permissions
|
||||
delete formValues.permissions_form
|
||||
}
|
||||
|
||||
var newObject = Object.assign(Object.assign({}, this.object), formValues)
|
||||
var serverResponse: Observable<T>
|
||||
switch (this.dialogMode) {
|
||||
case 'create':
|
||||
|
@@ -0,0 +1,19 @@
|
||||
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||
<app-permissions-select i18n-title title="Permissions" formControlName="permissions" [error]="error?.permissions"></app-permissions-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||
</div>
|
||||
</form>
|
@@ -0,0 +1,37 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { PaperlessGroup } from 'src/app/data/paperless-group'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-group-edit-dialog',
|
||||
templateUrl: './group-edit-dialog.component.html',
|
||||
styleUrls: ['./group-edit-dialog.component.scss'],
|
||||
})
|
||||
export class GroupEditDialogComponent extends EditDialogComponent<PaperlessGroup> {
|
||||
constructor(
|
||||
service: GroupService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal, userService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new user group`
|
||||
}
|
||||
|
||||
getEditTitle() {
|
||||
return $localize`Edit user group`
|
||||
}
|
||||
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
name: new FormControl(''),
|
||||
permissions: new FormControl(null),
|
||||
})
|
||||
}
|
||||
}
|
@@ -7,6 +7,7 @@ import {
|
||||
PaperlessMailAccount,
|
||||
} from 'src/app/data/paperless-mail-account'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
const IMAP_SECURITY_OPTIONS = [
|
||||
{ id: IMAPSecurity.None, name: $localize`No encryption` },
|
||||
@@ -20,8 +21,12 @@ const IMAP_SECURITY_OPTIONS = [
|
||||
styleUrls: ['./mail-account-edit-dialog.component.scss'],
|
||||
})
|
||||
export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> {
|
||||
constructor(service: MailAccountService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
constructor(
|
||||
service: MailAccountService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal, userService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
|
@@ -18,6 +18,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
const ATTACHMENT_TYPE_OPTIONS = [
|
||||
{
|
||||
@@ -113,9 +114,10 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMa
|
||||
activeModal: NgbActiveModal,
|
||||
accountService: MailAccountService,
|
||||
correspondentService: CorrespondentService,
|
||||
documentTypeService: DocumentTypeService
|
||||
documentTypeService: DocumentTypeService,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal)
|
||||
super(service, activeModal, userService)
|
||||
|
||||
accountService
|
||||
.listAll()
|
||||
|
@@ -16,6 +16,10 @@
|
||||
<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 *appIfOwner="object">
|
||||
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
|
@@ -5,6 +5,7 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-storage-path-edit-dialog',
|
||||
@@ -12,8 +13,12 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
styleUrls: ['./storage-path-edit-dialog.component.scss'],
|
||||
})
|
||||
export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> {
|
||||
constructor(service: StoragePathService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
constructor(
|
||||
service: StoragePathService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal, userService)
|
||||
}
|
||||
|
||||
get pathHint() {
|
||||
@@ -41,6 +46,7 @@ export class StoragePathEditDialogComponent extends EditDialogComponent<Paperles
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
permissions_form: new FormControl(null),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -13,6 +13,11 @@
|
||||
<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 *appIfOwner="object">
|
||||
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
|
@@ -6,6 +6,7 @@ import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { randomColor } from 'src/app/utils/color'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-tag-edit-dialog',
|
||||
@@ -13,8 +14,12 @@ import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
styleUrls: ['./tag-edit-dialog.component.scss'],
|
||||
})
|
||||
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
||||
constructor(service: TagService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
constructor(
|
||||
service: TagService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal, userService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
@@ -33,6 +38,7 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
permissions_form: new FormControl(null),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,38 @@
|
||||
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text>
|
||||
<app-input-text i18n-title title="Email" formControlName="email" [error]="error?.email"></app-input-text>
|
||||
<app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password>
|
||||
<app-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></app-input-text>
|
||||
<app-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></app-input-text>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="is_active" formControlName="is_active">
|
||||
<label class="form-check-label" for="is_active" i18n>Active</label>
|
||||
</div>
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="is_superuser" formControlName="is_superuser" (change)="onToggleSuperUser()">
|
||||
<label class="form-check-label" for="is_superuser"><ng-container i18n>Superuser</ng-container> <small class="form-text text-muted ms-1" i18n>(Grants all permissions and can view objects)</small></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-input-select i18n-title title="Groups" [items]="groups" multiple="true" formControlName="groups"></app-input-select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<app-permissions-select i18n-title title="Permissions" formControlName="user_permissions" [error]="error?.user_permissions" [inheritedPermissions]="inheritedPermissions"></app-permissions-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||
</div>
|
||||
</form>
|
@@ -0,0 +1,79 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { PaperlessGroup } from 'src/app/data/paperless-group'
|
||||
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-edit-dialog',
|
||||
templateUrl: './user-edit-dialog.component.html',
|
||||
styleUrls: ['./user-edit-dialog.component.scss'],
|
||||
})
|
||||
export class UserEditDialogComponent
|
||||
extends EditDialogComponent<PaperlessUser>
|
||||
implements OnInit
|
||||
{
|
||||
groups: PaperlessGroup[]
|
||||
|
||||
constructor(
|
||||
service: UserService,
|
||||
activeModal: NgbActiveModal,
|
||||
groupsService: GroupService
|
||||
) {
|
||||
super(service, activeModal, service)
|
||||
|
||||
groupsService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.groups = result.results))
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit()
|
||||
this.onToggleSuperUser()
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new user account`
|
||||
}
|
||||
|
||||
getEditTitle() {
|
||||
return $localize`Edit user account`
|
||||
}
|
||||
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
username: new FormControl(''),
|
||||
email: new FormControl(''),
|
||||
password: new FormControl(null),
|
||||
first_name: new FormControl(''),
|
||||
last_name: new FormControl(''),
|
||||
is_active: new FormControl(true),
|
||||
is_superuser: new FormControl(false),
|
||||
groups: new FormControl([]),
|
||||
user_permissions: new FormControl([]),
|
||||
})
|
||||
}
|
||||
|
||||
onToggleSuperUser() {
|
||||
if (this.objectForm.get('is_superuser').value) {
|
||||
this.objectForm.get('user_permissions').disable()
|
||||
} else {
|
||||
this.objectForm.get('user_permissions').enable()
|
||||
}
|
||||
}
|
||||
|
||||
get inheritedPermissions(): string[] {
|
||||
const groupsVal: Array<number> = this.objectForm.get('groups').value
|
||||
|
||||
if (!groupsVal) return []
|
||||
else
|
||||
return groupsVal.flatMap(
|
||||
(id) => this.groups.find((g) => g.id == id)?.permissions
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
||||
</svg>
|
||||
@@ -25,10 +25,10 @@
|
||||
</div>
|
||||
<div *ngIf="selectionModel.items" class="items">
|
||||
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText">
|
||||
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)"></app-toggleable-dropdown-button>
|
||||
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" [disabled]="disabled"></app-toggleable-dropdown-button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty">
|
||||
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
|
||||
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
|
||||
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
||||
|
@@ -317,6 +317,9 @@ export class FilterableDropdownComponent {
|
||||
@Input()
|
||||
applyOnClose = false
|
||||
|
||||
@Input()
|
||||
disabled = false
|
||||
|
||||
@Output()
|
||||
apply = new EventEmitter<ChangedItems>()
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<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)">
|
||||
<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">
|
||||
|
@@ -23,6 +23,9 @@ export class ToggleableDropdownButtonComponent {
|
||||
@Input()
|
||||
count: number
|
||||
|
||||
@Input()
|
||||
disabled: boolean = false
|
||||
|
||||
@Output()
|
||||
toggle = new EventEmitter()
|
||||
|
||||
|
@@ -3,8 +3,8 @@
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
|
||||
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
|
||||
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel">
|
||||
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button">
|
||||
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled">
|
||||
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error">
|
||||
<button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button>
|
||||
<input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
|
||||
<button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="disabled">+1</button>
|
||||
</div>
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
|
@@ -0,0 +1,68 @@
|
||||
<ng-container *ngIf="!accordion">
|
||||
<h5 i18n>Permissions</h5>
|
||||
<ng-container [ngTemplateOutlet]="permissionsForm"></ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="accordion">
|
||||
<ngb-accordion #acc="ngbAccordion" activeIds="">
|
||||
<ngb-panel i18n-title title="Edit Permissions">
|
||||
<ng-template ngbPanelContent>
|
||||
<ng-container [ngTemplateOutlet]="permissionsForm"></ng-container>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #permissionsForm>
|
||||
<div [formGroup]="form">
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2" i18n>Owner:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<app-input-select [items]="users" bindLabel="username" formControlName="owner" [allowNull]="true"></app-input-select>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
|
||||
<div formGroupName="set_permissions">
|
||||
<h6 class="mt-3" i18n>View</h6>
|
||||
<div formGroupName="view" class="mb-2">
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2" i18n>Users:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<app-permissions-user type="view" formControlName="users"></app-permissions-user>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2" i18n>Groups:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<app-permissions-group type="view" formControlName="groups"></app-permissions-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h6 class="mt-4" i18n>Edit</h6>
|
||||
<div formGroupName="change">
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2" i18n>Users:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<app-permissions-user type="change" formControlName="users"></app-permissions-user>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2" i18n>Groups:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<app-permissions-group type="change" formControlName="groups"></app-permissions-group>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
@@ -0,0 +1,69 @@
|
||||
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||
import { AbstractInputComponent } from '../../abstract-input'
|
||||
|
||||
export interface PermissionsFormObject {
|
||||
owner?: number
|
||||
set_permissions?: {
|
||||
view?: {
|
||||
users?: number[]
|
||||
groups?: number[]
|
||||
}
|
||||
change?: {
|
||||
users?: number[]
|
||||
groups?: number[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => PermissionsFormComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
selector: 'app-permissions-form',
|
||||
templateUrl: './permissions-form.component.html',
|
||||
styleUrls: ['./permissions-form.component.scss'],
|
||||
})
|
||||
export class PermissionsFormComponent
|
||||
extends AbstractInputComponent<PermissionsFormObject>
|
||||
implements OnInit
|
||||
{
|
||||
@Input()
|
||||
users: PaperlessUser[]
|
||||
|
||||
@Input()
|
||||
accordion: boolean = false
|
||||
|
||||
form = new FormGroup({
|
||||
owner: new FormControl(null),
|
||||
set_permissions: new FormGroup({
|
||||
view: new FormGroup({
|
||||
users: new FormControl([]),
|
||||
groups: new FormControl([]),
|
||||
}),
|
||||
change: new FormGroup({
|
||||
users: new FormControl([]),
|
||||
groups: new FormControl([]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form.valueChanges.subscribe((value) => {
|
||||
this.onChange(value)
|
||||
})
|
||||
}
|
||||
|
||||
writeValue(newValue: any): void {
|
||||
this.form.patchValue(newValue, { emitEvent: false })
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
<div class="paperless-input-select">
|
||||
<div>
|
||||
<ng-select name="inputId" [(ngModel)]="value"
|
||||
[disabled]="disabled"
|
||||
clearable="true"
|
||||
[items]="groups"
|
||||
multiple="true"
|
||||
bindLabel="name"
|
||||
bindValue="id"
|
||||
(change)="onChange(value)">
|
||||
</ng-select>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,30 @@
|
||||
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { PaperlessGroup } from 'src/app/data/paperless-group'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { AbstractInputComponent } from '../../abstract-input'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => PermissionsGroupComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
selector: 'app-permissions-group',
|
||||
templateUrl: './permissions-group.component.html',
|
||||
styleUrls: ['./permissions-group.component.scss'],
|
||||
})
|
||||
export class PermissionsGroupComponent extends AbstractInputComponent<PaperlessGroup> {
|
||||
groups: PaperlessGroup[]
|
||||
|
||||
constructor(groupService: GroupService) {
|
||||
super()
|
||||
groupService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.groups = result.results))
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
<div class="paperless-input-select">
|
||||
<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>
|
||||
</div>
|
@@ -0,0 +1,38 @@
|
||||
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 { SettingsService } from 'src/app/services/settings.service'
|
||||
import { AbstractInputComponent } from '../../abstract-input'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => PermissionsUserComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
selector: 'app-permissions-user',
|
||||
templateUrl: './permissions-user.component.html',
|
||||
styleUrls: ['./permissions-user.component.scss'],
|
||||
})
|
||||
export class PermissionsUserComponent extends AbstractInputComponent<
|
||||
PaperlessUser[]
|
||||
> {
|
||||
users: PaperlessUser[]
|
||||
|
||||
constructor(userService: UserService, settings: SettingsService) {
|
||||
super()
|
||||
userService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(result) =>
|
||||
(this.users = result.results.filter(
|
||||
(u) => u.id !== settings.currentUser.id
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
<div class="mb-3 paperless-input-select">
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<div class="mb-3 paperless-input-select" [class.disabled]="disabled">
|
||||
<label *ngIf="title" class="form-label" [for]="inputId">{{title}}</label>
|
||||
<div [class.input-group]="allowCreateNew">
|
||||
<ng-select name="inputId" [(ngModel)]="value"
|
||||
[disabled]="disabled"
|
||||
@@ -11,7 +11,8 @@
|
||||
addTagText="Add item"
|
||||
i18n-addTagText="Used for both types, correspondents, storage paths"
|
||||
[placeholder]="placeholder"
|
||||
bindLabel="name"
|
||||
[multiple]="multiple"
|
||||
[bindLabel]="bindLabel"
|
||||
bindValue="id"
|
||||
(change)="onChange(value)"
|
||||
(search)="onSearch($event)"
|
||||
@@ -19,7 +20,7 @@
|
||||
(clear)="clearLastSearchTerm()"
|
||||
(blur)="onBlur()">
|
||||
</ng-select>
|
||||
<button *ngIf="allowCreateNew" class="btn btn-outline-secondary" type="button" (click)="addItem()">
|
||||
<button *ngIf="allowCreateNew" class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||
</svg>
|
||||
|
@@ -1 +1,14 @@
|
||||
// styles for ng-select child are in styles.scss
|
||||
.paperless-input-select.disabled {
|
||||
.input-group {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
::ng-deep ng-select {
|
||||
pointer-events: none;
|
||||
|
||||
.ng-select-container {
|
||||
background-color: var(--pngx-bg-alt) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -44,6 +44,12 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
||||
@Input()
|
||||
placeholder: string
|
||||
|
||||
@Input()
|
||||
multiple: boolean = false
|
||||
|
||||
@Input()
|
||||
bindLabel: string = 'name'
|
||||
|
||||
@Output()
|
||||
createNew = new EventEmitter<string>()
|
||||
|
||||
|
@@ -1,8 +1,9 @@
|
||||
<div class="mb-3 paperless-input-select paperless-input-tags">
|
||||
<div class="mb-3 paperless-input-select paperless-input-tags" [class.disabled]="disabled">
|
||||
<label class="form-label" for="tags" i18n>Tags</label>
|
||||
|
||||
<div class="input-group flex-nowrap">
|
||||
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value"
|
||||
[disabled]="disabled"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="false"
|
||||
[clearSearchOnAdd]="true"
|
||||
@@ -31,7 +32,7 @@
|
||||
</ng-template>
|
||||
</ng-select>
|
||||
|
||||
<button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()">
|
||||
<button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||
</svg>
|
||||
|
@@ -10,3 +10,17 @@
|
||||
.tag-wrap-delete {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.paperless-input-select.disabled {
|
||||
.input-group {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
::ng-deep ng-select {
|
||||
pointer-events: none;
|
||||
|
||||
.ng-select-container {
|
||||
background-color: var(--pngx-bg-alt) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -74,6 +74,8 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
}
|
||||
|
||||
removeTag(event: PointerEvent, id: number) {
|
||||
if (this.disabled) return
|
||||
|
||||
// prevent opening dropdown
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
|
||||
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
|
||||
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
|
@@ -0,0 +1,18 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="cancelClicked()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<p class="mb-3" *ngIf="message" [innerHTML]="message | safeHtml"></p>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<app-permissions-form [users]="users" formControlName="permissions_form"></app-permissions-form>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-primary" (click)="cancelClicked()" i18n>Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="confirmClicked.emit(permissions)" i18n>Confirm</button>
|
||||
</div>
|
@@ -0,0 +1,46 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-permissions-dialog',
|
||||
templateUrl: './permissions-dialog.component.html',
|
||||
styleUrls: ['./permissions-dialog.component.scss'],
|
||||
})
|
||||
export class PermissionsDialogComponent {
|
||||
users: PaperlessUser[]
|
||||
|
||||
constructor(
|
||||
public activeModal: NgbActiveModal,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.userService.listAll().subscribe((r) => (this.users = r.results))
|
||||
}
|
||||
|
||||
@Output()
|
||||
public confirmClicked = new EventEmitter()
|
||||
|
||||
@Input()
|
||||
title = $localize`Set Permissions`
|
||||
|
||||
form = new FormGroup({
|
||||
permissions_form: new FormControl(),
|
||||
})
|
||||
|
||||
get permissions() {
|
||||
return {
|
||||
owner: this.form.get('permissions_form').value?.owner ?? null,
|
||||
set_permissions:
|
||||
this.form.get('permissions_form').value?.set_permissions ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
message = $localize`Note that permissions set here will override any existing permissions`
|
||||
|
||||
cancelClicked() {
|
||||
this.activeModal.close()
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
<form [formGroup]="form" [class.opacity-50]="disabled">
|
||||
<label class="form-label">{{title}}</label>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item d-flex">
|
||||
<div class="col-3" i18n>Type</div>
|
||||
<div class="col" i18n>All</div>
|
||||
<div class="col" i18n>Add</div>
|
||||
<div class="col" i18n>Change</div>
|
||||
<div class="col" i18n>Delete</div>
|
||||
<div class="col" i18n>View</div>
|
||||
</li>
|
||||
<li class="list-group-item d-flex" *ngFor="let type of PermissionType | keyvalue" [formGroupName]="type.key">
|
||||
<div class="col-3">{{type.key}}:</div>
|
||||
|
||||
<div class="col form-check form-check-inline form-switch" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key)" placement="left" triggers="mouseenter:mouseleave">
|
||||
<input type="checkbox" class="form-check-input" id="{{type.key}}_all" (change)="toggleAll($event, type.key)" [checked]="typesWithAllActions.has(type.key) || isInherited(type.key)" [attr.disabled]="disabled || isInherited(type.key) ? true : null">
|
||||
<label class="form-check-label visually-hidden" for="{{type.key}}_all" i18n>All</label>
|
||||
</div>
|
||||
|
||||
<div *ngFor="let action of PermissionAction | keyvalue" class="col form-check form-check-inline" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key, action.key)" placement="left" triggers="mouseenter:mouseleave">
|
||||
<input type="checkbox" class="form-check-input" id="{{type.key}}_{{action.key}}" formControlName="{{action.key}}" [attr.disabled]="isDisabled(type.key, action.key)">
|
||||
<label class="form-check-label visually-hidden" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label>
|
||||
</div>
|
||||
</li>
|
||||
<div *ngIf="error" class="invalid-feedback d-block">{{error}}</div>
|
||||
</ul>
|
||||
</form>
|
@@ -0,0 +1,189 @@
|
||||
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => PermissionsSelectComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
selector: 'app-permissions-select',
|
||||
templateUrl: './permissions-select.component.html',
|
||||
styleUrls: ['./permissions-select.component.scss'],
|
||||
})
|
||||
export class PermissionsSelectComponent
|
||||
implements OnInit, ControlValueAccessor
|
||||
{
|
||||
PermissionType = PermissionType
|
||||
PermissionAction = PermissionAction
|
||||
|
||||
@Input()
|
||||
title: string = 'Permissions'
|
||||
|
||||
@Input()
|
||||
error: string
|
||||
|
||||
permissions: string[]
|
||||
|
||||
form = new FormGroup({})
|
||||
|
||||
typesWithAllActions: Set<string> = new Set()
|
||||
|
||||
_inheritedPermissions: string[] = []
|
||||
|
||||
@Input()
|
||||
set inheritedPermissions(inherited: string[]) {
|
||||
// remove <app_label>. from permission strings
|
||||
const newInheritedPermissions = inherited?.length
|
||||
? inherited.map((p) => p.replace(/^\w+\./g, ''))
|
||||
: []
|
||||
|
||||
if (this._inheritedPermissions !== newInheritedPermissions) {
|
||||
this._inheritedPermissions = newInheritedPermissions
|
||||
this.writeValue(this.permissions) // updates visual checks etc.
|
||||
}
|
||||
}
|
||||
|
||||
inheritedWarning: string = $localize`Inerhited from group`
|
||||
|
||||
constructor(private readonly permissionsService: PermissionsService) {
|
||||
for (const type in PermissionType) {
|
||||
const control = new FormGroup({})
|
||||
for (const action in PermissionAction) {
|
||||
control.addControl(action, new FormControl(null))
|
||||
}
|
||||
this.form.addControl(type, control)
|
||||
}
|
||||
}
|
||||
|
||||
writeValue(permissions: string[]): void {
|
||||
this.permissions = permissions ?? []
|
||||
const allPerms = this._inheritedPermissions.concat(this.permissions)
|
||||
|
||||
allPerms.forEach((permissionStr) => {
|
||||
const { actionKey, typeKey } =
|
||||
this.permissionsService.getPermissionKeys(permissionStr)
|
||||
|
||||
if (actionKey && typeKey) {
|
||||
if (this.form.get(typeKey)?.get(actionKey)) {
|
||||
this.form
|
||||
.get(typeKey)
|
||||
.get(actionKey)
|
||||
.patchValue(true, { emitEvent: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
Object.keys(PermissionType).forEach((type) => {
|
||||
if (
|
||||
Object.values(this.form.get(type).value).every((val) => val == true)
|
||||
) {
|
||||
this.typesWithAllActions.add(type)
|
||||
} else {
|
||||
this.typesWithAllActions.delete(type)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onChange = (newValue: string[]) => {}
|
||||
|
||||
onTouched = () => {}
|
||||
|
||||
disabled: boolean = false
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn
|
||||
}
|
||||
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form.valueChanges.subscribe((newValue) => {
|
||||
let permissions = []
|
||||
Object.entries(newValue).forEach(([typeKey, typeValue]) => {
|
||||
// e.g. [Document, { Add: true, View: true ... }]
|
||||
const selectedActions = Object.entries(typeValue).filter(
|
||||
([actionKey, actionValue]) => actionValue == true
|
||||
)
|
||||
|
||||
selectedActions.forEach(([actionKey, actionValue]) => {
|
||||
permissions.push(
|
||||
(PermissionType[typeKey] as string).replace(
|
||||
'%s',
|
||||
PermissionAction[actionKey]
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
if (selectedActions.length == Object.entries(typeValue).length) {
|
||||
this.typesWithAllActions.add(typeKey)
|
||||
} else {
|
||||
this.typesWithAllActions.delete(typeKey)
|
||||
}
|
||||
})
|
||||
this.onChange(permissions)
|
||||
})
|
||||
}
|
||||
|
||||
toggleAll(event, type) {
|
||||
const typeGroup = this.form.get(type)
|
||||
if (event.target.checked) {
|
||||
Object.keys(PermissionAction).forEach((action) => {
|
||||
typeGroup.get(action).patchValue(true)
|
||||
})
|
||||
this.typesWithAllActions.add(type)
|
||||
} else {
|
||||
Object.keys(PermissionAction).forEach((action) => {
|
||||
typeGroup.get(action).patchValue(false)
|
||||
})
|
||||
this.typesWithAllActions.delete(type)
|
||||
}
|
||||
}
|
||||
|
||||
isInherited(typeKey: string, actionKey: string = null) {
|
||||
if (this._inheritedPermissions.length == 0) return false
|
||||
else if (actionKey) {
|
||||
return this._inheritedPermissions.includes(
|
||||
this.permissionsService.getPermissionCode(
|
||||
PermissionAction[actionKey],
|
||||
PermissionType[typeKey]
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return Object.values(PermissionAction).every((action) => {
|
||||
return this._inheritedPermissions.includes(
|
||||
this.permissionsService.getPermissionCode(
|
||||
action as PermissionAction,
|
||||
PermissionType[typeKey]
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// if checkbox is disabled either because "All", inhereted or entire component disabled
|
||||
isDisabled(typeKey: string, actionKey: string) {
|
||||
return this.typesWithAllActions.has(typeKey) ||
|
||||
this.isInherited(typeKey, actionKey) ||
|
||||
this.disabled
|
||||
? true
|
||||
: null
|
||||
}
|
||||
}
|
@@ -28,12 +28,14 @@
|
||||
|
||||
<app-welcome-widget *ngIf="settingsService.offerTour()" tourAnchor="tour.dashboard"></app-welcome-widget>
|
||||
|
||||
<ng-container *ngFor="let v of savedViewService.dashboardViews; first as isFirst">
|
||||
<app-saved-view-widget *ngIf="isFirst; else noTour" [savedView]="v" tourAnchor="tour.dashboard"></app-saved-view-widget>
|
||||
<ng-template #noTour>
|
||||
<app-saved-view-widget [savedView]="v"></app-saved-view-widget>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||
<ng-container *ngFor="let v of savedViewService.dashboardViews; first as isFirst">
|
||||
<app-saved-view-widget *ngIf="isFirst; else noTour" [savedView]="v" tourAnchor="tour.dashboard"></app-saved-view-widget>
|
||||
<ng-template #noTour>
|
||||
<app-saved-view-widget [savedView]="v"></app-saved-view-widget>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
|
@@ -1,17 +1,20 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: ['./dashboard.component.scss'],
|
||||
})
|
||||
export class DashboardComponent {
|
||||
export class DashboardComponent extends ComponentWithPermissions {
|
||||
constructor(
|
||||
public savedViewService: SavedViewService,
|
||||
public settingsService: SettingsService
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get subtitle() {
|
||||
if (this.settingsService.displayName) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<app-widget-frame [title]="savedView.name" [loading]="loading">
|
||||
|
||||
<a class="btn-link" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
|
||||
<a class="btn-link" header-buttons [routerLink]="[]" (click)="showAll()" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" i18n>Show all</a>
|
||||
|
||||
|
||||
<table content class="table table-sm table-hover table-borderless mb-0">
|
||||
@@ -10,7 +10,7 @@
|
||||
<th scope="col" i18n>Title</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<tr *ngFor="let doc of documents">
|
||||
<td><a routerLink="/documents/{{doc.id}}" class="d-block text-dark text-decoration-none">{{doc.created_date | customDate}}</a></td>
|
||||
<td><a routerLink="/documents/{{doc.id}}" class="d-block text-dark text-decoration-none">{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t, $event)"></app-tag></a></td>
|
||||
|
@@ -9,13 +9,17 @@ import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-saved-view-widget',
|
||||
templateUrl: './saved-view-widget.component.html',
|
||||
styleUrls: ['./saved-view-widget.component.scss'],
|
||||
})
|
||||
export class SavedViewWidgetComponent implements OnInit, OnDestroy {
|
||||
export class SavedViewWidgetComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
loading: boolean = true
|
||||
|
||||
constructor(
|
||||
@@ -24,7 +28,9 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy {
|
||||
private list: DocumentListViewService,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
public openDocumentsService: OpenDocumentsService
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@Input()
|
||||
savedView: PaperlessSavedView
|
||||
@@ -74,6 +80,7 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy {
|
||||
|
||||
clickTag(tag: PaperlessTag, event: MouseEvent) {
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
this.list.quickFilter([
|
||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: tag.id.toString() },
|
||||
|
@@ -9,7 +9,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div content tourAnchor="tour.upload-widget">
|
||||
<form>
|
||||
<form *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Document }">
|
||||
<ngx-file-drop dropZoneLabel="Drop documents here or" browseBtnLabel="Browse files" (onFileDrop)="dropped($event)"
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card"
|
||||
multiple="true" contentClassName="justify-content-center d-flex align-items-center py-5 px-2" [showBrowseBtn]=true
|
||||
@@ -40,13 +40,15 @@
|
||||
<h6 class="alert-heading">{{status.filename}}</h6>
|
||||
<p class="mb-0 pb-1" *ngIf="!isFinished(status) || (isFinished(status) && !status.documentId)">{{status.message}}</p>
|
||||
<ngb-progressbar [value]="status.getProgress()" [max]="1" [type]="getStatusColor(status)"></ngb-progressbar>
|
||||
<div *ngIf="isFinished(status)">
|
||||
<button *ngIf="status.documentId" class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
|
||||
<small i18n>Open document</small>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<div *ngIf="isFinished(status)">
|
||||
<button *ngIf="status.documentId" class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
|
||||
<small i18n>Open document</small>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ngb-alert>
|
||||
</ng-template>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NgxFileDropEntry } from 'ngx-file-drop'
|
||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
@@ -14,13 +15,15 @@ const MAX_ALERTS = 5
|
||||
templateUrl: './upload-file-widget.component.html',
|
||||
styleUrls: ['./upload-file-widget.component.scss'],
|
||||
})
|
||||
export class UploadFileWidgetComponent {
|
||||
export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
alertsExpanded = false
|
||||
|
||||
constructor(
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private uploadDocumentsService: UploadDocumentsService
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.consumerStatusService.getConsumerStatus().slice(0, MAX_ALERTS)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div *ngIf="comments">
|
||||
<form [formGroup]="commentForm" class="needs-validation mt-3" novalidate>
|
||||
<form [formGroup]="commentForm" class="needs-validation mt-3" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Comment }" novalidate>
|
||||
<div class="form-group">
|
||||
<textarea class="form-control form-control-sm" [class.is-invalid]="newCommentError" rows="3" formControlName="newComment" placeholder="Enter comment" i18n-placeholder required></textarea>
|
||||
<div class="invalid-feedback" i18n>
|
||||
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
<div class="d-flex card-footer small bg-light text-primary justify-content-between align-items-center">
|
||||
<span>{{displayName(comment)}} - {{ comment.created | customDate}}</span>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 fade" (click)="deleteComment(comment.id)">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 fade" (click)="deleteComment(comment.id)" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Comment }">
|
||||
<svg width="13" height="13" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg>
|
||||
|
@@ -4,13 +4,14 @@ import { PaperlessDocumentComment } from 'src/app/data/paperless-document-commen
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-comments',
|
||||
templateUrl: './document-comments.component.html',
|
||||
styleUrls: ['./document-comments.component.scss'],
|
||||
})
|
||||
export class DocumentCommentsComponent {
|
||||
export class DocumentCommentsComponent extends ComponentWithPermissions {
|
||||
commentForm: FormGroup = new FormGroup({
|
||||
newComment: new FormControl(''),
|
||||
})
|
||||
@@ -32,7 +33,9 @@ export class DocumentCommentsComponent {
|
||||
constructor(
|
||||
private commentsService: DocumentCommentsService,
|
||||
private toastService: ToastService
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
update(): void {
|
||||
this.networkActive = true
|
||||
@@ -89,8 +92,8 @@ export class DocumentCommentsComponent {
|
||||
displayName(comment: PaperlessDocumentComment): string {
|
||||
if (!comment.user) return ''
|
||||
let nameComponents = []
|
||||
if (comment.user.firstname) nameComponents.unshift(comment.user.firstname)
|
||||
if (comment.user.lastname) nameComponents.unshift(comment.user.lastname)
|
||||
if (comment.user.first_name) nameComponents.unshift(comment.user.first_name)
|
||||
if (comment.user.last_name) nameComponents.unshift(comment.user.last_name)
|
||||
if (comment.user.username) {
|
||||
if (nameComponents.length > 0)
|
||||
nameComponents.push(`(${comment.user.username})`)
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-danger me-2 ms-auto" (click)="delete()">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger me-2 ms-auto" (click)="delete()" [disabled]="!userIsOwner">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span>
|
||||
@@ -20,7 +20,7 @@
|
||||
</a>
|
||||
|
||||
<div class="btn-group" ngbDropdown role="group" *ngIf="metadata?.has_archive_version">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle></button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
|
||||
</div>
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="redoOcr()">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="redoOcr()" [disabled]="!userCanEdit">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" />
|
||||
</svg><span class="d-none d-lg-inline ps-1" i18n>Redo OCR</span>
|
||||
@@ -170,19 +170,31 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="5" *ngIf="commentsEnabled">
|
||||
<a ngbNavLink i18n>Comments</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-document-comments [documentId]="documentId"></app-document-comments>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="6" *appIfOwner="document">
|
||||
<a ngbNavLink i18n>Permissions</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="mb-3">
|
||||
<app-permissions-form [users]="users" formControlName="permissions_form"></app-permissions-form>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="networkActive || (isDirty$ | async) !== true">Discard</button>
|
||||
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive || (isDirty$ | async) !== true || error">Save & next</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive || (isDirty$ | async) !== true || error">Save</button>
|
||||
<ng-container>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button>
|
||||
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true || error">Save & next</button>
|
||||
<button type="submit" class="btn btn-primary" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true || error">Save</button>
|
||||
</ng-container>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
@@ -35,6 +35,13 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-detail',
|
||||
@@ -58,6 +65,7 @@ export class DocumentDetailComponent
|
||||
document: PaperlessDocument
|
||||
metadata: PaperlessDocumentMetadata
|
||||
suggestions: PaperlessDocumentSuggestions
|
||||
users: PaperlessUser[]
|
||||
|
||||
title: string
|
||||
titleSubject: Subject<string> = new Subject()
|
||||
@@ -78,6 +86,7 @@ export class DocumentDetailComponent
|
||||
storage_path: new FormControl(),
|
||||
archive_serial_number: new FormControl(),
|
||||
tags: new FormControl([]),
|
||||
permissions_form: new FormControl(null),
|
||||
})
|
||||
|
||||
previewCurrentPage: number = 1
|
||||
@@ -106,6 +115,9 @@ export class DocumentDetailComponent
|
||||
}
|
||||
}
|
||||
|
||||
PermissionAction = PermissionAction
|
||||
PermissionType = PermissionType
|
||||
|
||||
constructor(
|
||||
private documentsService: DocumentService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -118,7 +130,9 @@ export class DocumentDetailComponent
|
||||
private documentTitlePipe: DocumentTitlePipe,
|
||||
private toastService: ToastService,
|
||||
private settings: SettingsService,
|
||||
private storagePathService: StoragePathService
|
||||
private storagePathService: StoragePathService,
|
||||
private permissionsService: PermissionsService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
titleKeyUp(event) {
|
||||
@@ -147,7 +161,13 @@ export class DocumentDetailComponent
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.error = null
|
||||
Object.assign(this.document, this.documentForm.value)
|
||||
const docValues = Object.assign({}, this.documentForm.value)
|
||||
docValues['owner'] =
|
||||
this.documentForm.get('permissions_form').value['owner']
|
||||
docValues['set_permissions'] =
|
||||
this.documentForm.get('permissions_form').value['set_permissions']
|
||||
delete docValues['permissions_form']
|
||||
Object.assign(this.document, docValues)
|
||||
})
|
||||
|
||||
this.correspondentService
|
||||
@@ -165,6 +185,11 @@ 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),
|
||||
@@ -232,6 +257,10 @@ export class DocumentDetailComponent
|
||||
storage_path: doc.storage_path,
|
||||
archive_serial_number: doc.archive_serial_number,
|
||||
tags: [...doc.tags],
|
||||
permissions_form: {
|
||||
owner: doc.owner,
|
||||
set_permissions: doc.permissions,
|
||||
},
|
||||
})
|
||||
|
||||
this.isDirty$ = dirtyCheck(
|
||||
@@ -286,7 +315,14 @@ export class DocumentDetailComponent
|
||||
},
|
||||
})
|
||||
this.title = this.documentTitlePipe.transform(doc.title)
|
||||
this.documentForm.patchValue(doc)
|
||||
const docFormValues = Object.assign({}, doc)
|
||||
docFormValues['permissions_form'] = {
|
||||
owner: doc.owner,
|
||||
set_permissions: doc.permissions,
|
||||
}
|
||||
|
||||
this.documentForm.patchValue(docFormValues, { emitEvent: false })
|
||||
if (!this.userCanEdit) this.documentForm.disable()
|
||||
}
|
||||
|
||||
createDocumentType(newName: string) {
|
||||
@@ -378,7 +414,7 @@ export class DocumentDetailComponent
|
||||
.update(this.document)
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
next: () => {
|
||||
this.close()
|
||||
this.networkActive = false
|
||||
this.error = null
|
||||
@@ -564,6 +600,36 @@ export class DocumentDetailComponent
|
||||
}
|
||||
|
||||
get commentsEnabled(): boolean {
|
||||
return this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED)
|
||||
return (
|
||||
this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED) &&
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.Document
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
get userIsOwner(): boolean {
|
||||
let doc: PaperlessDocument = Object.assign({}, this.document)
|
||||
// dont disable while editing
|
||||
if (this.document && this.store?.value.owner) {
|
||||
doc.owner = this.store?.value.owner
|
||||
}
|
||||
return !this.document || this.permissionsService.currentUserOwnsObject(doc)
|
||||
}
|
||||
|
||||
get userCanEdit(): boolean {
|
||||
let doc: PaperlessDocument = Object.assign({}, this.document)
|
||||
// dont disable while editing
|
||||
if (this.document && this.store?.value.owner) {
|
||||
doc.owner = this.store?.value.owner
|
||||
}
|
||||
return (
|
||||
!this.document ||
|
||||
this.permissionsService.currentUserHasObjectPermissions(
|
||||
PermissionAction.Change,
|
||||
doc
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -23,11 +23,12 @@
|
||||
</div>
|
||||
<div class="w-100 d-xl-none"></div>
|
||||
<div class="col-auto mb-2 mb-xl-0">
|
||||
<div class="d-flex">
|
||||
<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
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[multiple]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
@@ -38,6 +39,7 @@
|
||||
<app-filterable-dropdown class="me-2 me-md-3" title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
(opened)="openCorrespondentDropdown()"
|
||||
@@ -47,6 +49,7 @@
|
||||
<app-filterable-dropdown class="me-2 me-md-3" title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
(opened)="openDocumentTypeDropdown()"
|
||||
@@ -56,6 +59,7 @@
|
||||
<app-filterable-dropdown class="me-2 me-md-3" title="Storage path" icon="folder-fill" i18n-title
|
||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||
[items]="storagePaths"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
(opened)="openStoragePathDropdown()"
|
||||
@@ -65,7 +69,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
<div class="btn-toolbar me-2">
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
|
||||
</svg> <ng-container i18n>Permissions</ng-container>
|
||||
</button>
|
||||
|
||||
<div ngbDropdown class="me-2 d-flex">
|
||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
@@ -74,7 +85,7 @@
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||
<button ngbDropdownItem (click)="redoOcrSelected()" i18n>Redo OCR</button>
|
||||
<button ngbDropdownItem (click)="redoOcrSelected()" [disabled]="!userCanEditAll" i18n>Redo OCR</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,7 +131,7 @@
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()">
|
||||
<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" />
|
||||
</svg> <ng-container i18n>Delete</ng-container>
|
||||
|
@@ -25,6 +25,9 @@ import { saveAs } from 'file-saver'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { first, Subject, takeUntil } from 'rxjs'
|
||||
|
||||
@@ -33,7 +36,10 @@ import { first, Subject, takeUntil } from 'rxjs'
|
||||
templateUrl: './bulk-editor.component.html',
|
||||
styleUrls: ['./bulk-editor.component.scss'],
|
||||
})
|
||||
export class BulkEditorComponent implements OnInit, OnDestroy {
|
||||
export class BulkEditorComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
tags: PaperlessTag[]
|
||||
correspondents: PaperlessCorrespondent[]
|
||||
documentTypes: PaperlessDocumentType[]
|
||||
@@ -63,8 +69,11 @@ export class BulkEditorComponent implements OnInit, OnDestroy {
|
||||
private openDocumentService: OpenDocumentsService,
|
||||
private settings: SettingsService,
|
||||
private toastService: ToastService,
|
||||
private storagePathService: StoragePathService
|
||||
) {}
|
||||
private storagePathService: StoragePathService,
|
||||
private permissionService: PermissionsService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
applyOnClose: boolean = this.settings.get(
|
||||
SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE
|
||||
@@ -73,6 +82,25 @@ export class BulkEditorComponent implements OnInit, OnDestroy {
|
||||
SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS
|
||||
)
|
||||
|
||||
get userCanEditAll(): boolean {
|
||||
let canEdit: boolean = true
|
||||
const docs = this.list.documents.filter((d) => this.list.selected.has(d.id))
|
||||
canEdit = docs.every((d) =>
|
||||
this.permissionService.currentUserHasObjectPermissions(
|
||||
this.PermissionAction.Change,
|
||||
d
|
||||
)
|
||||
)
|
||||
return canEdit
|
||||
}
|
||||
|
||||
get userOwnsAll(): boolean {
|
||||
let ownsAll: boolean = true
|
||||
const docs = this.list.documents.filter((d) => this.list.selected.has(d.id))
|
||||
ownsAll = docs.every((d) => this.permissionService.currentUserOwnsObject(d))
|
||||
return ownsAll
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.tagService
|
||||
.listAll()
|
||||
@@ -463,4 +491,14 @@ export class BulkEditorComponent implements OnInit, OnDestroy {
|
||||
this.executeBulkOperation(modal, 'redo_ocr', {})
|
||||
})
|
||||
}
|
||||
|
||||
setPermissions() {
|
||||
let modal = this.modalService.open(PermissionsDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.confirmClicked.subscribe((permissions) => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.executeBulkOperation(modal, 'set_permissions', permissions)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -43,7 +43,7 @@
|
||||
<use xlink:href="assets/bootstrap-icons.svg#diagram-3"/>
|
||||
</svg> <span class="d-none d-md-inline" i18n>More like this</span>
|
||||
</a>
|
||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
|
||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
||||
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#pencil"/>
|
||||
</svg> <span class="d-none d-md-inline" i18n>Edit</span>
|
||||
|
@@ -10,6 +10,7 @@ import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-card-large',
|
||||
@@ -19,11 +20,13 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
'../popover-preview/popover-preview.scss',
|
||||
],
|
||||
})
|
||||
export class DocumentCardLargeComponent {
|
||||
export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
private settingsService: SettingsService
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@Input()
|
||||
selected = false
|
||||
|
@@ -67,7 +67,7 @@
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group w-100">
|
||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title>
|
||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n-title>
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
|
@@ -11,6 +11,7 @@ import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-card-small',
|
||||
@@ -20,11 +21,13 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
'../popover-preview/popover-preview.scss',
|
||||
],
|
||||
})
|
||||
export class DocumentCardSmallComponent {
|
||||
export class DocumentCardSmallComponent extends ComponentWithPermissions {
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
private settingsService: SettingsService
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@Input()
|
||||
selected = false
|
||||
|
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group ms-2 flex-fill" ngbDropdown role="group">
|
||||
<div class="btn-group ms-2 flex-fill" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }" ngbDropdown role="group">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle>
|
||||
<ng-container i18n>Views</ng-container>
|
||||
<div *ngIf="savedViewIsModified" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle">
|
||||
@@ -72,8 +72,10 @@
|
||||
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
||||
</ng-container>
|
||||
|
||||
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button>
|
||||
<button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button>
|
||||
<div *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.SavedView }">
|
||||
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button>
|
||||
</div>
|
||||
<button ngbDropdownItem (click)="saveViewConfigAs()" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.SavedView }" i18n>Save as...</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -30,6 +30,7 @@ import {
|
||||
} from 'src/app/services/rest/document.service'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||
import { FilterEditorComponent } from './filter-editor/filter-editor.component'
|
||||
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'
|
||||
|
||||
@@ -38,7 +39,10 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi
|
||||
templateUrl: './document-list.component.html',
|
||||
styleUrls: ['./document-list.component.scss'],
|
||||
})
|
||||
export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
export class DocumentListComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
constructor(
|
||||
public list: DocumentListViewService,
|
||||
public savedViewService: SavedViewService,
|
||||
@@ -48,7 +52,9 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
private modalService: NgbModal,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
public openDocumentsService: OpenDocumentsService
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@ViewChild('filterEditor')
|
||||
private filterEditor: FilterEditorComponent
|
||||
|
@@ -4,6 +4,10 @@ import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import {
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||
@@ -21,6 +25,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles
|
||||
modalService: NgbModal,
|
||||
toastService: ToastService,
|
||||
documentListViewService: DocumentListViewService,
|
||||
permissionsService: PermissionsService,
|
||||
private datePipe: CustomDatePipe
|
||||
) {
|
||||
super(
|
||||
@@ -29,9 +34,11 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles
|
||||
CorrespondentEditDialogComponent,
|
||||
toastService,
|
||||
documentListViewService,
|
||||
permissionsService,
|
||||
FILTER_CORRESPONDENT,
|
||||
$localize`correspondent`,
|
||||
$localize`correspondents`,
|
||||
PermissionType.Correspondent,
|
||||
[
|
||||
{
|
||||
key: 'last_correspondence',
|
||||
|
@@ -3,6 +3,10 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import {
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||
@@ -18,7 +22,8 @@ export class DocumentTypeListComponent extends ManagementListComponent<Paperless
|
||||
documentTypeService: DocumentTypeService,
|
||||
modalService: NgbModal,
|
||||
toastService: ToastService,
|
||||
documentListViewService: DocumentListViewService
|
||||
documentListViewService: DocumentListViewService,
|
||||
permissionsService: PermissionsService
|
||||
) {
|
||||
super(
|
||||
documentTypeService,
|
||||
@@ -26,9 +31,11 @@ export class DocumentTypeListComponent extends ManagementListComponent<Paperless
|
||||
DocumentTypeEditDialogComponent,
|
||||
toastService,
|
||||
documentListViewService,
|
||||
permissionsService,
|
||||
FILTER_DOCUMENT_TYPE,
|
||||
$localize`document type`,
|
||||
$localize`document types`,
|
||||
PermissionType.DocumentType,
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<app-page-header title="{{ typeNamePlural | titlecase }}">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
|
||||
<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">
|
||||
@@ -41,24 +41,24 @@
|
||||
</svg>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||
<button (click)="filterDocuments(object)" ngbDropdownItem i18n>Filter Documents</button>
|
||||
<button (click)="openEditDialog(object)" ngbDropdownItem i18n>Edit</button>
|
||||
<button class="text-danger" (click)="openDeleteDialog(object)" ngbDropdownItem i18n>Delete</button>
|
||||
<button (click)="filterDocuments(object)" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents</button>
|
||||
<button (click)="openEditDialog(object)" *appIfPermissions="{ action: PermissionAction.Change, type: permissionType }" ngbDropdownItem i18n>Edit</button>
|
||||
<button class="text-danger" (click)="openDeleteDialog(object)" *appIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" ngbDropdownItem i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group d-none d-sm-block">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object)">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object)" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
||||
</svg> <ng-container i18n>Documents</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object)">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object)" *appIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object)">
|
||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object)" *appIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||
|
@@ -14,14 +14,20 @@ import {
|
||||
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 {
|
||||
SortableDirective,
|
||||
SortEvent,
|
||||
} from 'src/app/directives/sortable.directive'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import {
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { AbstractNameFilterService } from 'src/app/services/rest/abstract-name-filter-service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
|
||||
export interface ManagementListColumn {
|
||||
key: string
|
||||
@@ -35,6 +41,7 @@ export interface ManagementListColumn {
|
||||
|
||||
@Directive()
|
||||
export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
constructor(
|
||||
@@ -43,11 +50,15 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
private editDialogComponent: any,
|
||||
private toastService: ToastService,
|
||||
private documentListViewService: DocumentListViewService,
|
||||
private permissionsService: PermissionsService,
|
||||
protected filterRuleType: number,
|
||||
public typeName: string,
|
||||
public typeNamePlural: string,
|
||||
public permissionType: PermissionType,
|
||||
public extraColumns: ManagementListColumn[]
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>
|
||||
|
||||
@@ -209,4 +220,15 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
onNameFilterKeyUp(event: KeyboardEvent) {
|
||||
if (event.code == 'Escape') this.nameFilterDebounce.next(null)
|
||||
}
|
||||
|
||||
userCanDelete(object: ObjectWithPermissions): boolean {
|
||||
return this.permissionsService.currentUserOwnsObject(object)
|
||||
}
|
||||
|
||||
userCanEdit(object: ObjectWithPermissions): boolean {
|
||||
return this.permissionsService.currentUserHasObjectPermissions(
|
||||
this.PermissionAction.Change,
|
||||
object
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<app-page-header title="Settings" i18n-title>
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
|
||||
<a class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
|
||||
<a *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
|
||||
<ng-container i18n>Open Django Admin</ng-container>
|
||||
<svg class="sidebaricon ms-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/>
|
||||
@@ -219,7 +219,7 @@
|
||||
|
||||
<div class="mb-2 col-auto">
|
||||
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" i18n>Delete</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -235,80 +235,84 @@
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="SettingsNavIDs.Mail" (mouseover)="maybeInitializeTab(SettingsNavIDs.Mail)" (focusin)="maybeInitializeTab(SettingsNavIDs.Mail)">
|
||||
<li *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }" [ngbNavItem]="SettingsNavIDs.Mail" (mouseover)="maybeInitializeTab(SettingsNavIDs.Mail)" (focusin)="maybeInitializeTab(SettingsNavIDs.Mail)">
|
||||
<a ngbNavLink i18n>Mail</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<ng-container *ngIf="mailAccounts && mailRules">
|
||||
<h4>
|
||||
<ng-container i18n>Mail accounts</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Account</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group" formGroupName="mailAccounts">
|
||||
<ng-container *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }">
|
||||
<h4>
|
||||
<ng-container i18n>Mail accounts</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Account</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group" formGroupName="mailAccounts">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Server</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Server</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let account of mailAccounts" class="list-group-item" [formGroupName]="account.id">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)">{{account.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{account.imap_server}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary" type="button" (click)="editMailAccount(account)" i18n>Edit</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailAccount(account)" i18n>Delete</button>
|
||||
<li *ngFor="let account of mailAccounts" class="list-group-item" [formGroupName]="account.id">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)">{{account.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{account.imap_server}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" class="btn btn-sm btn-primary" type="button" (click)="editMailAccount(account)" i18n>Edit</button>
|
||||
<button *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailAccount(account)" i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</li>
|
||||
|
||||
<div *ngIf="mailAccounts.length === 0" i18n>No mail accounts defined.</div>
|
||||
</ul>
|
||||
<div *ngIf="mailAccounts.length === 0" i18n>No mail accounts defined.</div>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
<h4 class="mt-4">
|
||||
<ng-container i18n>Mail rules</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Rule</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group" formGroupName="mailRules">
|
||||
<ng-container *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }">
|
||||
<h4 class="mt-4">
|
||||
<ng-container i18n>Mail rules</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Rule</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group" formGroupName="mailRules">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Account</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Account</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let rule of mailRules" class="list-group-item" [formGroupName]="rule.id">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)">{{rule.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary" type="button" (click)="editMailRule(rule)" i18n>Edit</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)" i18n>Delete</button>
|
||||
<li *ngFor="let rule of mailRules" class="list-group-item" [formGroupName]="rule.id">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)">{{rule.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" class="btn btn-sm btn-primary" type="button" (click)="editMailRule(rule)" i18n>Edit</button>
|
||||
<button *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)" i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</li>
|
||||
|
||||
<div *ngIf="mailRules.length === 0" i18n>No mail rules defined.</div>
|
||||
</ul>
|
||||
<div *ngIf="mailRules.length === 0" i18n>No mail rules defined.</div>
|
||||
</ul>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="!mailAccounts || !mailRules">
|
||||
@@ -318,9 +322,95 @@
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="SettingsNavIDs.UsersGroups" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" (mouseover)="maybeInitializeTab(SettingsNavIDs.UsersGroups)" (focusin)="maybeInitializeTab(SettingsNavIDs.UsersGroups)">
|
||||
<a ngbNavLink i18n>Users & Groups</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<ng-container *ngIf="users && groups">
|
||||
<h4 class="d-flex">
|
||||
<ng-container i18n>Users</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editUser()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add User</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group" formGroupName="usersGroup">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Username</div>
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Groups</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let user of users" class="list-group-item" [formGroupName]="user.id">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editUser(user)">{{user.username}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
|
||||
<div class="col d-flex align-items-center">{{user.groups?.map(getGroupName, this).join(', ')}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary" type="button" (click)="editUser(user)" i18n>Edit</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4 class="mt-4 d-flex">
|
||||
<ng-container i18n>Groups</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editGroup()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Group</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul *ngIf="groups.length > 0" class="list-group" formGroupName="groupsGroup">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let group of groups" class="list-group-item" [formGroupName]="group.id">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)">{{group.name}}</button></div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary" type="button" (click)="editGroup(group)" i18n>Edit</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div *ngIf="groups.length === 0">No groups defined</div>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="!users || !groups">
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||
<button type="submit" class="btn btn-primary mb-2" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||
</form>
|
||||
|
@@ -29,15 +29,21 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { ViewportScroller } from '@angular/common'
|
||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
import { NgbModal, NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||
import { PaperlessGroup } from 'src/app/data/paperless-group'
|
||||
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
||||
import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account'
|
||||
import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
||||
import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
enum SettingsNavIDs {
|
||||
General = 1,
|
||||
@@ -53,12 +59,15 @@ enum SettingsNavIDs {
|
||||
styleUrls: ['./settings.component.scss'],
|
||||
})
|
||||
export class SettingsComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
|
||||
{
|
||||
SettingsNavIDs = SettingsNavIDs
|
||||
activeNavID: number
|
||||
|
||||
savedViewGroup = new FormGroup({})
|
||||
usersGroup = new FormGroup({})
|
||||
groupsGroup = new FormGroup({})
|
||||
|
||||
mailAccountGroup = new FormGroup({})
|
||||
mailRuleGroup = new FormGroup({})
|
||||
@@ -83,6 +92,8 @@ export class SettingsComponent
|
||||
notificationsConsumerSuccess: new FormControl(null),
|
||||
notificationsConsumerFailed: new FormControl(null),
|
||||
notificationsConsumerSuppressOnDashboard: new FormControl(null),
|
||||
usersGroup: this.usersGroup,
|
||||
groupsGroup: this.groupsGroup,
|
||||
|
||||
savedViewsWarnOnUnsavedChange: new FormControl(null),
|
||||
savedViews: this.savedViewGroup,
|
||||
@@ -103,6 +114,9 @@ export class SettingsComponent
|
||||
unsubscribeNotifier: Subject<any> = new Subject()
|
||||
savePending: boolean = false
|
||||
|
||||
users: PaperlessUser[]
|
||||
groups: PaperlessGroup[]
|
||||
|
||||
get computedDateLocale(): string {
|
||||
return (
|
||||
this.settingsForm.value.dateLocale ||
|
||||
@@ -121,10 +135,13 @@ export class SettingsComponent
|
||||
@Inject(LOCALE_ID) public currentLocale: string,
|
||||
private viewportScroller: ViewportScroller,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private router: Router,
|
||||
public readonly tourService: TourService,
|
||||
private usersService: UserService,
|
||||
private groupsService: GroupService,
|
||||
private router: Router,
|
||||
private modalService: NgbModal
|
||||
) {
|
||||
super()
|
||||
this.settings.settingsSaved.subscribe(() => {
|
||||
if (!this.savePending) this.initialize()
|
||||
})
|
||||
@@ -198,6 +215,8 @@ export class SettingsComponent
|
||||
savedViewsWarnOnUnsavedChange: this.settings.get(
|
||||
SETTINGS_KEYS.SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE
|
||||
),
|
||||
usersGroup: {},
|
||||
groupsGroup: {},
|
||||
savedViews: {},
|
||||
mailAccounts: {},
|
||||
mailRules: {},
|
||||
@@ -229,6 +248,17 @@ export class SettingsComponent
|
||||
this.savedViews = r.results
|
||||
this.initialize(false)
|
||||
})
|
||||
} else if (
|
||||
navID == SettingsNavIDs.UsersGroups &&
|
||||
(!this.users || !this.groups)
|
||||
) {
|
||||
this.usersService.listAll().subscribe((r) => {
|
||||
this.users = r.results
|
||||
this.groupsService.listAll().subscribe((r) => {
|
||||
this.groups = r.results
|
||||
this.initialize(false)
|
||||
})
|
||||
})
|
||||
} else if (
|
||||
navID == SettingsNavIDs.Mail &&
|
||||
(!this.mailAccounts || !this.mailRules)
|
||||
@@ -271,6 +301,50 @@ export class SettingsComponent
|
||||
}
|
||||
}
|
||||
|
||||
if (this.users && this.groups) {
|
||||
for (let user of this.users) {
|
||||
storeData.usersGroup[user.id.toString()] = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
is_active: user.is_active,
|
||||
is_superuser: user.is_superuser,
|
||||
groups: user.groups,
|
||||
user_permissions: user.user_permissions,
|
||||
}
|
||||
this.usersGroup.addControl(
|
||||
user.id.toString(),
|
||||
new FormGroup({
|
||||
id: new FormControl(null),
|
||||
username: new FormControl(null),
|
||||
first_name: new FormControl(null),
|
||||
last_name: new FormControl(null),
|
||||
is_active: new FormControl(null),
|
||||
is_superuser: new FormControl(null),
|
||||
groups: new FormControl(null),
|
||||
user_permissions: new FormControl(null),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
for (let group of this.groups) {
|
||||
storeData.groupsGroup[group.id.toString()] = {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
permissions: group.permissions,
|
||||
}
|
||||
this.groupsGroup.addControl(
|
||||
group.id.toString(),
|
||||
new FormGroup({
|
||||
id: new FormControl(null),
|
||||
name: new FormControl(null),
|
||||
permissions: new FormControl(null),
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mailAccounts && this.mailRules) {
|
||||
for (let account of this.mailAccounts) {
|
||||
storeData.mailAccounts[account.id.toString()] = {
|
||||
@@ -547,6 +621,120 @@ export class SettingsComponent
|
||||
this.settingsForm.get('themeColor').patchValue('')
|
||||
}
|
||||
|
||||
editUser(user: PaperlessUser) {
|
||||
var modal = this.modalService.open(UserEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
})
|
||||
modal.componentInstance.dialogMode = user ? 'edit' : 'create'
|
||||
modal.componentInstance.object = user
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (newUser) => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Saved user "${newUser.username}".`
|
||||
)
|
||||
this.usersService.listAll().subscribe((r) => {
|
||||
this.users = r.results
|
||||
this.initialize()
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error saving user: ${e.toString()}.`
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
deleteUser(user: PaperlessUser) {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm delete user account`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently this user account.`
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.usersService.delete(user).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted user`)
|
||||
this.usersService.listAll().subscribe((r) => {
|
||||
this.users = r.results
|
||||
this.initialize()
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting user: ${e.toString()}.`
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
editGroup(group: PaperlessGroup) {
|
||||
var modal = this.modalService.open(GroupEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
size: 'lg',
|
||||
})
|
||||
modal.componentInstance.dialogMode = group ? 'edit' : 'create'
|
||||
modal.componentInstance.object = group
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (newGroup) => {
|
||||
this.toastService.showInfo($localize`Saved group "${newGroup.name}".`)
|
||||
this.groupsService.listAll().subscribe((r) => {
|
||||
this.groups = r.results
|
||||
this.initialize()
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error saving group: ${e.toString()}.`
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
deleteGroup(group: PaperlessGroup) {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm delete user group`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently this user group.`
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.groupsService.delete(group).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted group`)
|
||||
this.groupsService.listAll().subscribe((r) => {
|
||||
this.groups = r.results
|
||||
this.initialize()
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting group: ${e.toString()}.`
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getGroupName(id: number): string {
|
||||
return this.groups?.find((g) => g.id === id)?.name ?? ''
|
||||
}
|
||||
|
||||
editMailAccount(account: PaperlessMailAccount) {
|
||||
const modal = this.modalService.open(MailAccountEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
|
@@ -3,6 +3,10 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { FILTER_STORAGE_PATH } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import {
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
@@ -18,7 +22,8 @@ export class StoragePathListComponent extends ManagementListComponent<PaperlessS
|
||||
directoryService: StoragePathService,
|
||||
modalService: NgbModal,
|
||||
toastService: ToastService,
|
||||
documentListViewService: DocumentListViewService
|
||||
documentListViewService: DocumentListViewService,
|
||||
permissionsService: PermissionsService
|
||||
) {
|
||||
super(
|
||||
directoryService,
|
||||
@@ -26,9 +31,11 @@ export class StoragePathListComponent extends ManagementListComponent<PaperlessS
|
||||
StoragePathEditDialogComponent,
|
||||
toastService,
|
||||
documentListViewService,
|
||||
permissionsService,
|
||||
FILTER_STORAGE_PATH,
|
||||
$localize`storage path`,
|
||||
$localize`storage paths`,
|
||||
PermissionType.StoragePath,
|
||||
[
|
||||
{
|
||||
key: 'path',
|
||||
|
@@ -3,6 +3,10 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import {
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||
@@ -18,7 +22,8 @@ export class TagListComponent extends ManagementListComponent<PaperlessTag> {
|
||||
tagService: TagService,
|
||||
modalService: NgbModal,
|
||||
toastService: ToastService,
|
||||
documentListViewService: DocumentListViewService
|
||||
documentListViewService: DocumentListViewService,
|
||||
permissionsService: PermissionsService
|
||||
) {
|
||||
super(
|
||||
tagService,
|
||||
@@ -26,9 +31,11 @@ export class TagListComponent extends ManagementListComponent<PaperlessTag> {
|
||||
TagEditDialogComponent,
|
||||
toastService,
|
||||
documentListViewService,
|
||||
permissionsService,
|
||||
FILTER_HAS_TAGS_ALL,
|
||||
$localize`tag`,
|
||||
$localize`tags`,
|
||||
PermissionType.Tag,
|
||||
[
|
||||
{
|
||||
key: 'color',
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Clear selection</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" [disabled]="tasksService.total === 0">
|
||||
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check2-all"/>
|
||||
</svg> <ng-container i18n>{{dismissButtonText}}</ng-container>
|
||||
@@ -75,16 +75,18 @@
|
||||
</td>
|
||||
<td scope="row">
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg> <ng-container i18n>Dismiss</ng-container>
|
||||
</button>
|
||||
<button *ngIf="task.related_document" class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||
</svg> <ng-container i18n>Open Document</ng-container>
|
||||
</button>
|
||||
<ng-container *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<button *ngIf="task.related_document" class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||
</svg> <ng-container i18n>Open Document</ng-container>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@@ -5,13 +5,17 @@ import { Subject, first } from 'rxjs'
|
||||
import { PaperlessTask } from 'src/app/data/paperless-task'
|
||||
import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-tasks',
|
||||
templateUrl: './tasks.component.html',
|
||||
styleUrls: ['./tasks.component.scss'],
|
||||
})
|
||||
export class TasksComponent implements OnInit, OnDestroy {
|
||||
export class TasksComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
public activeTab: string
|
||||
public selectedTasks: Set<number> = new Set()
|
||||
private unsubscribeNotifer = new Subject()
|
||||
@@ -27,7 +31,9 @@ export class TasksComponent implements OnInit, OnDestroy {
|
||||
public tasksService: TasksService,
|
||||
private modalService: NgbModal,
|
||||
private readonly router: Router
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.tasksService.reload()
|
||||
|
@@ -0,0 +1,9 @@
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
|
||||
export class ComponentWithPermissions {
|
||||
public readonly PermissionAction = PermissionAction
|
||||
public readonly PermissionType = PermissionType
|
||||
}
|
Reference in New Issue
Block a user