mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge pull request #1886 from paperless-ngx/1.9.2-ui-tweaks
Feature: 1.9.2 UI tweaks
This commit is contained in:
		| @@ -24,6 +24,7 @@ import { CorrespondentEditDialogComponent } from './components/common/edit-dialo | |||||||
| import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' | import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' | ||||||
| import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component' | import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component' | ||||||
| import { TagComponent } from './components/common/tag/tag.component' | import { TagComponent } from './components/common/tag/tag.component' | ||||||
|  | import { ClearableBadge } from './components/common/clearable-badge/clearable-badge.component' | ||||||
| import { PageHeaderComponent } from './components/common/page-header/page-header.component' | import { PageHeaderComponent } from './components/common/page-header/page-header.component' | ||||||
| import { AppFrameComponent } from './components/app-frame/app-frame.component' | import { AppFrameComponent } from './components/app-frame/app-frame.component' | ||||||
| import { ToastsComponent } from './components/common/toasts/toasts.component' | import { ToastsComponent } from './components/common/toasts/toasts.component' | ||||||
| @@ -142,6 +143,7 @@ function initializeApp(settings: SettingsService) { | |||||||
|     DocumentTypeEditDialogComponent, |     DocumentTypeEditDialogComponent, | ||||||
|     StoragePathEditDialogComponent, |     StoragePathEditDialogComponent, | ||||||
|     TagComponent, |     TagComponent, | ||||||
|  |     ClearableBadge, | ||||||
|     PageHeaderComponent, |     PageHeaderComponent, | ||||||
|     AppFrameComponent, |     AppFrameComponent, | ||||||
|     ToastsComponent, |     ToastsComponent, | ||||||
|   | |||||||
| @@ -16,7 +16,12 @@ | |||||||
|         <use xlink:href="assets/bootstrap-icons.svg#search"/> |         <use xlink:href="assets/bootstrap-icons.svg#search"/> | ||||||
|       </svg> |       </svg> | ||||||
|       <input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search" |       <input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search" | ||||||
|         [formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)" i18n-placeholder> |         [formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)" (selectItem)="itemSelected($event)" i18n-placeholder> | ||||||
|  |       <button *ngIf="!searchFieldEmpty" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()"> | ||||||
|  |         <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |           <path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> | ||||||
|  |         </svg> | ||||||
|  |       </button> | ||||||
|     </form> |     </form> | ||||||
|   </div> |   </div> | ||||||
|   <ul ngbNav class="order-sm-3"> |   <ul ngbNav class="order-sm-3"> | ||||||
|   | |||||||
| @@ -243,17 +243,18 @@ main { | |||||||
|  |  | ||||||
|   form { |   form { | ||||||
|     position: relative; |     position: relative; | ||||||
|  |  | ||||||
|  |     > svg { | ||||||
|  |       position: absolute; | ||||||
|  |       left: 0.6rem; | ||||||
|  |       top: 0.5rem; | ||||||
|  |       color: rgba(255, 255, 255, 0.6); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   svg { |  | ||||||
|     position: absolute; |  | ||||||
|     left: 0.6rem; |  | ||||||
|     top: 0.5rem; |  | ||||||
|     color: rgba(255, 255, 255, 0.6); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &:focus-within { |   &:focus-within { | ||||||
|     svg { |     form > svg { | ||||||
|       display: none; |       display: none; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -93,6 +93,20 @@ export class AppFrameComponent implements OnInit, ComponentCanDeactivate { | |||||||
|  |  | ||||||
|   searchField = new FormControl('') |   searchField = new FormControl('') | ||||||
|  |  | ||||||
|  |   get searchFieldEmpty(): boolean { | ||||||
|  |     return this.searchField.value.trim().length == 0 | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   resetSearchField() { | ||||||
|  |     this.searchField.reset('') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   searchFieldKeyup(event: KeyboardEvent) { | ||||||
|  |     if (event.key == 'Escape') { | ||||||
|  |       this.resetSearchField() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   get openDocuments(): PaperlessDocument[] { |   get openDocuments(): PaperlessDocument[] { | ||||||
|     return this.openDocumentsService.getOpenDocuments() |     return this.openDocumentsService.getOpenDocuments() | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | <button *ngIf="active" class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)"> | ||||||
|  |     <svg *ngIf="!isNumbered && selected" width="1em" height="1em" class="check m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |         <use xlink:href="assets/bootstrap-icons.svg#check-lg"/> | ||||||
|  |     </svg> | ||||||
|  |     <div *ngIf="isNumbered" class="number">{{number}}<span class="visually-hidden">selected</span></div> | ||||||
|  |     <svg width=".9em" height="1em" class="x m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |         <use xlink:href="assets/bootstrap-icons.svg#x-lg"/> | ||||||
|  |     </svg> | ||||||
|  | </button> | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | .badge { | ||||||
|  |     min-width: 20px; | ||||||
|  |     min-height: 20px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .x { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .number { | ||||||
|  |     min-width: 1em; | ||||||
|  |     min-height: 1em; | ||||||
|  |     display: inline-block; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | button:hover { | ||||||
|  |     .check, | ||||||
|  |     .number { | ||||||
|  |         opacity: 0 !important; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .x { | ||||||
|  |         display: inline-block; | ||||||
|  |         position: absolute; | ||||||
|  |         top: 5px; | ||||||
|  |         left: calc(50% - 4px); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,33 @@ | |||||||
|  | import { Component, Input, Output, EventEmitter } from '@angular/core' | ||||||
|  |  | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-clearable-badge', | ||||||
|  |   templateUrl: './clearable-badge.component.html', | ||||||
|  |   styleUrls: ['./clearable-badge.component.scss'], | ||||||
|  | }) | ||||||
|  | export class ClearableBadge { | ||||||
|  |   constructor() {} | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   number: number | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   selected: boolean | ||||||
|  |  | ||||||
|  |   @Output() | ||||||
|  |   cleared: EventEmitter<boolean> = new EventEmitter() | ||||||
|  |  | ||||||
|  |   get active(): boolean { | ||||||
|  |     return this.selected || this.number > -1 | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get isNumbered(): boolean { | ||||||
|  |     return this.number > -1 | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   onClick(event: PointerEvent) { | ||||||
|  |     this.cleared.emit(true) | ||||||
|  |     event.stopImmediatePropagation() | ||||||
|  |     event.preventDefault() | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,9 +1,7 @@ | |||||||
|   <div class="btn-group w-100" ngbDropdown role="group"> |   <div class="btn-group w-100" ngbDropdown role="group"> | ||||||
|   <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'"> |   <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'"> | ||||||
|     {{title}} |     {{title}} | ||||||
|     <div *ngIf="isActive" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle"> |     <app-clearable-badge [selected]="isActive" (cleared)="reset()"></app-clearable-badge><span class="visually-hidden">selected</span> | ||||||
|       <span class="visually-hidden">selected</span> |  | ||||||
|     </div> |  | ||||||
|   </button> |   </button> | ||||||
|   <div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> |   <div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> | ||||||
|     <div class="list-group list-group-flush"> |     <div class="list-group list-group-flush"> | ||||||
|   | |||||||
| @@ -106,6 +106,13 @@ export class DateDropdownComponent implements OnInit, OnDestroy { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   reset() { | ||||||
|  |     this.dateBefore = null | ||||||
|  |     this.dateAfter = null | ||||||
|  |     this.relativeDate = null | ||||||
|  |     this.onChange() | ||||||
|  |   } | ||||||
|  |  | ||||||
|   setRelativeDate(rd: RelativeDate) { |   setRelativeDate(rd: RelativeDate) { | ||||||
|     this.dateBefore = null |     this.dateBefore = null | ||||||
|     this.dateAfter = null |     this.dateAfter = null | ||||||
|   | |||||||
| @@ -5,12 +5,7 @@ | |||||||
|     </svg> |     </svg> | ||||||
|     <div class="d-none d-sm-inline"> {{title}}</div> |     <div class="d-none d-sm-inline"> {{title}}</div> | ||||||
|     <ng-container *ngIf="!editing && selectionModel.selectionSize() > 0"> |     <ng-container *ngIf="!editing && selectionModel.selectionSize() > 0"> | ||||||
|       <div *ngIf="multiple" class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light text-light rounded-pill"> |       <app-clearable-badge [number]="multiple ? selectionModel.totalCount : undefined" [selected]="!multiple && selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge> | ||||||
|         {{selectionModel.totalCount}}<span class="visually-hidden">selected</span> |  | ||||||
|       </div> |  | ||||||
|       <div *ngIf="!multiple" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle"> |  | ||||||
|         <span class="visually-hidden">selected</span> |  | ||||||
|       </div> |  | ||||||
|     </ng-container> |     </ng-container> | ||||||
|   </button> |   </button> | ||||||
|   <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> |   <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> | ||||||
|   | |||||||
| @@ -384,4 +384,9 @@ export class FilterableDropdownComponent { | |||||||
|       this.selectionModel.exclude(itemID) |       this.selectionModel.exclude(itemID) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   reset() { | ||||||
|  |     this.selectionModel.reset() | ||||||
|  |     this.selectionModelChange.emit(this.selectionModel) | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,7 +11,12 @@ | |||||||
|           <select *ngIf="textFilterTarget == 'asn'" class="form-select flex-grow-0 w-auto" [(ngModel)]="textFilterModifier" (change)="textFilterModifierChange()"> |           <select *ngIf="textFilterTarget == 'asn'" class="form-select flex-grow-0 w-auto" [(ngModel)]="textFilterModifier" (change)="textFilterModifierChange()"> | ||||||
|             <option *ngFor="let m of textFilterModifiers" ngbDropdownItem [value]="m.id">{{m.label}}</option> |             <option *ngFor="let m of textFilterModifiers" ngbDropdownItem [value]="m.id">{{m.label}}</option> | ||||||
|           </select> |           </select> | ||||||
|           <input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup.enter)="textFilterEnter()" [readonly]="textFilterTarget == 'fulltext-morelike'"> |           <button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()"> | ||||||
|  |             <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |               <path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> | ||||||
|  |             </svg> | ||||||
|  |           </button> | ||||||
|  |           <input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget == 'fulltext-morelike'"> | ||||||
|          </div> |          </div> | ||||||
|      </div> |      </div> | ||||||
|   </div> |   </div> | ||||||
|   | |||||||
| @@ -21,3 +21,7 @@ | |||||||
| input[type="text"] { | input[type="text"] { | ||||||
|   min-width: 120px; |   min-width: 120px; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .z-10 { | ||||||
|  |   z-index: 10; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -709,15 +709,23 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|     this.updateRules() |     this.updateRules() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   textFilterEnter() { |   textFilterKeyup(event: KeyboardEvent) { | ||||||
|     const filterString = ( |     if (event.key == 'Enter') { | ||||||
|       this.textFilterInput.nativeElement as HTMLInputElement |       const filterString = ( | ||||||
|     ).value |         this.textFilterInput.nativeElement as HTMLInputElement | ||||||
|     if (filterString.length) { |       ).value | ||||||
|       this.updateTextFilter(filterString) |       if (filterString.length) { | ||||||
|  |         this.updateTextFilter(filterString) | ||||||
|  |       } | ||||||
|  |     } else if (event.key == 'Escape') { | ||||||
|  |       this.resetTextField() | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   resetTextField() { | ||||||
|  |     this.updateTextFilter('') | ||||||
|  |   } | ||||||
|  |  | ||||||
|   changeTextFilterTarget(target) { |   changeTextFilterTarget(target) { | ||||||
|     if ( |     if ( | ||||||
|       this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE && |       this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE && | ||||||
|   | |||||||
| @@ -587,7 +587,7 @@ a.badge { | |||||||
|     border-radius: 0.15rem; |     border-radius: 0.15rem; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   > .btn:not(:first-child) { |   > .btn:not(:first-child):not(:nth-child(2)) { | ||||||
|     border-top-left-radius: 0; |     border-top-left-radius: 0; | ||||||
|     border-bottom-left-radius: 0; |     border-bottom-left-radius: 0; | ||||||
|   } |   } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon