Saving work on trash

[skip ci]
This commit is contained in:
shamoon
2024-04-23 00:07:33 -07:00
parent 06bb218c71
commit fc1423057d
19 changed files with 472 additions and 19 deletions

View File

@@ -26,6 +26,7 @@ import { MailComponent } from './components/manage/mail/mail.component'
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
import { ConfigComponent } from './components/admin/config/config.component'
import { TrashComponent } from './components/admin/trash/trash.component'
export const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
@@ -144,6 +145,14 @@ export const routes: Routes = [
requireAdmin: true,
},
},
{
path: 'trash',
component: TrashComponent,
canActivate: [PermissionsGuard],
data: {
requireAdmin: true,
},
},
// redirect old paths
{
path: 'settings/mail',

View File

@@ -357,6 +357,7 @@ import localeSv from '@angular/common/locales/sv'
import localeTr from '@angular/common/locales/tr'
import localeUk from '@angular/common/locales/uk'
import localeZh from '@angular/common/locales/zh'
import { TrashComponent } from './components/admin/trash/trash.component'
registerLocaleData(localeAf)
registerLocaleData(localeAr)
@@ -497,6 +498,7 @@ function initializeApp(settings: SettingsService) {
GlobalSearchComponent,
HotkeyDialogComponent,
DeletePagesConfirmDialogComponent,
TrashComponent,
],
imports: [
BrowserModule,

View File

@@ -0,0 +1,94 @@
<pngx-page-header
title="Trash"
i18n-title
info="Manage trashed items."
>
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Clear selection</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash(selectedObjects)" [disabled]="selectedObjects.size === 0">
<i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Delete objects</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash()" [disabled]="trashedObjects.length === 0">
<i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Empty trash</ng-container>
</button>
</pngx-page-header>
<div class="row mb-3">
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="trashedObjects.length" [(page)]="page" [maxSize]="5" (pageChange)="reload()" size="sm" aria-label="Pagination"></ngb-pagination>
</div>
<div class="card border table-responsive mb-3">
<table class="table table-striped align-middle shadow-sm mb-0">
<thead>
<tr>
<th scope="col">
<div class="form-check m-0 ms-2 me-n2">
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="togggleAll" [disabled]="trashedObjects.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
<label class="form-check-label" for="all-objects"></label>
</div>
</th>
<th scope="col" class="fw-normal" i18n>Name</th>
<th scope="col" class="fw-normal d-none d-sm-table-cell" i18n>Deleted</th>
<th scope="col" class="fw-normal" i18n>Actions</th>
</tr>
</thead>
<tbody>
@if (isLoading) {
<tr>
<td colspan="5">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</td>
</tr>
}
@for (object of trashedObjects; track object.id) {
<tr (click)="toggleSelected(object); $event.stopPropagation();">
<td>
<div class="form-check m-0 ms-2 me-n2">
<input type="checkbox" class="form-check-input" id="{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();">
<label class="form-check-label" for="{{object.id}}"></label>
</div>
</td>
<td scope="row">{{ object['name'] ?? object['title'] }}</td>
<td scope="row">{{ object['deleted_at'] | customDate }}</td>
<td scope="row">
<div class="btn-group d-block d-sm-none">
<div ngbDropdown container="body" class="d-inline-block">
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
<i-bs name="three-dots-vertical"></i-bs>
</button>
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
<button (click)="restoreObject(object)" ngbDropdownItem i18n>Restore</button>
<button (click)="deleteObject(object)" 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)="restoreObject(object); $event.stopPropagation();">
<i-bs width="1em" height="1em" name="restore"></i-bs>&nbsp;<ng-container i18n>Restore</ng-container>
</button>
<button class="btn btn-sm btn-outline-danger" (click)="deleteObject(object); $event.stopPropagation();">
<i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
@if (!isLoading) {
<div class="d-flex mb-2">
<div>
<ng-container i18n>{trashedObjects.length, plural, =1 {One object in trash} other {{{trashedObjects.length || 0}} total objects in trash}}</ng-container>
@if (selectedObjects.size > 0) {
&nbsp;({{selectedObjects.size}} selected)
}
</div>
@if (trashedObjects.length > 20) {
<ngb-pagination class="ms-auto" [pageSize]="25" [collectionSize]="trashedObjects.length" [(page)]="page" [maxSize]="5" (pageChange)="reload()" size="sm" aria-label="Pagination"></ngb-pagination>
}
</div>
}

View File

@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { TrashComponent } from './trash.component'
describe('TrashComponent', () => {
let component: TrashComponent
let fixture: ComponentFixture<TrashComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TrashComponent],
}).compileComponents()
fixture = TestBed.createComponent(TrashComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})

View File

@@ -0,0 +1,87 @@
import { HttpClient } from '@angular/common/http'
import { Component } from '@angular/core'
import { ObjectWithId } from 'src/app/data/object-with-id'
import { ToastService } from 'src/app/services/toast.service'
import { TrashService } from 'src/app/services/trash.service'
import { environment } from 'src/environments/environment'
@Component({
selector: 'pngx-trash',
templateUrl: './trash.component.html',
styleUrl: './trash.component.scss',
})
export class TrashComponent {
public trashedObjects: ObjectWithId[] = []
public selectedObjects: Set<number> = new Set()
public togggleAll: boolean = false
public page: number = 1
public isLoading: boolean = false
constructor(
private trashService: TrashService,
private toastService: ToastService
) {
this.reload()
}
reload() {
this.isLoading = true
this.trashService.getTrash().subscribe((trash) => {
this.trashedObjects = trash
this.isLoading = false
console.log('Trash:', trash)
})
}
deleteObject(object: ObjectWithId) {
this.trashService.emptyTrash([object.id]).subscribe(() => {
this.toastService.showInfo($localize`Object deleted`)
this.reload()
})
}
emptyTrash(objects: Set<number> = null) {
console.log('Emptying trash')
this.trashService
.emptyTrash(objects ? Array.from(objects) : [])
.subscribe(() => {
this.toastService.showInfo($localize`Object(s) deleted`)
this.reload()
})
}
restoreObject(object: ObjectWithId) {
this.trashService.restoreObjects([object.id]).subscribe(() => {
this.toastService.showInfo($localize`Object restored`)
this.reload()
})
}
restoreAll(objects: Set<number> = null) {
this.trashService
.restoreObjects(objects ? Array.from(this.selectedObjects) : [])
.subscribe(() => {
this.toastService.showInfo($localize`Object(s) restored`)
this.reload()
})
}
toggleAll(event: PointerEvent) {
if ((event.target as HTMLInputElement).checked) {
this.selectedObjects = new Set(this.trashedObjects.map((t) => t.id))
} else {
this.clearSelection()
}
}
toggleSelected(object: ObjectWithId) {
this.selectedObjects.has(object.id)
? this.selectedObjects.delete(object.id)
: this.selectedObjects.add(object.id)
}
clearSelection() {
this.togggleAll = false
this.selectedObjects.clear()
}
}

View File

@@ -267,6 +267,15 @@
</a>
</li>
}
<!-- @if (permissionsService.isAdmin()) { -->
<li class="nav-item app-link">
<a class="nav-link" routerLink="trash" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Trash"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="trash"></i-bs><span>&nbsp;<ng-container i18n>Trash</ng-container></span>
</a>
</li>
<!-- } -->
<li class="nav-item mt-2" tourAnchor="tour.outro">
<a class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap text-decoration-none"
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"

View File

@@ -708,12 +708,10 @@ export class BulkEditorComponent
let modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.delayConfirm(5)
modal.componentInstance.title = $localize`Delete confirm`
modal.componentInstance.messageBold = $localize`This operation will permanently delete ${this.list.selected.size} selected document(s).`
modal.componentInstance.message = $localize`This operation cannot be undone.`
modal.componentInstance.messageBold = $localize`Move ${this.list.selected.size} selected document(s) to the trash?`
modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Delete document(s)`
modal.componentInstance.btnCaption = $localize`Move to trash`
modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {

View File

@@ -144,6 +144,8 @@ export interface Document extends ObjectWithPermissions {
added?: Date
deleted_at?: Date
original_file_name?: string
archived_file_name?: string

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing'
import { TrashService } from './trash.service'
describe('TrashServiceService', () => {
let service: TrashService
beforeEach(() => {
TestBed.configureTestingModule({})
service = TestBed.inject(TrashService)
})
it('should be created', () => {
expect(service).toBeTruthy()
})
})

View File

@@ -0,0 +1,30 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { environment } from 'src/environments/environment'
import { ObjectWithId } from '../data/object-with-id'
@Injectable({
providedIn: 'root',
})
export class TrashService {
constructor(private http: HttpClient) {}
public getTrash(): Observable<ObjectWithId[]> {
return this.http.get<ObjectWithId[]>(`${environment.apiBaseUrl}trash/`)
}
public emptyTrash(documents: number[] = []) {
return this.http.post(`${environment.apiBaseUrl}trash/`, {
action: 'empty',
documents,
})
}
public restoreObjects(documents: number[]): Observable<any> {
return this.http.post(`${environment.apiBaseUrl}trash/`, {
action: 'restore',
documents,
})
}
}