Saving work on trash
[skip ci]
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
94
src-ui/src/app/components/admin/trash/trash.component.html
Normal file
94
src-ui/src/app/components/admin/trash/trash.component.html
Normal 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> <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> <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> <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> <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> <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) {
|
||||
({{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>
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
87
src-ui/src/app/components/admin/trash/trash.component.ts
Normal file
87
src-ui/src/app/components/admin/trash/trash.component.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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> <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"
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -144,6 +144,8 @@ export interface Document extends ObjectWithPermissions {
|
||||
|
||||
added?: Date
|
||||
|
||||
deleted_at?: Date
|
||||
|
||||
original_file_name?: string
|
||||
|
||||
archived_file_name?: string
|
||||
|
||||
16
src-ui/src/app/services/trash.service.spec.ts
Normal file
16
src-ui/src/app/services/trash.service.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
30
src-ui/src/app/services/trash.service.ts
Normal file
30
src-ui/src/app/services/trash.service.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user