mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 19:17:13 -05:00 
			
		
		
		
	Enhancement: improved loading visuals (#8435)
This commit is contained in:
		
							parent
							
								
									8722ff481c
								
							
						
					
					
						commit
						0647812699
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -47,7 +47,7 @@
 | 
			
		||||
                </tr>
 | 
			
		||||
            }
 | 
			
		||||
            @for (document of documentsInTrash; track document.id) {
 | 
			
		||||
                <tr (click)="toggleSelected(document); $event.stopPropagation();" (mouseleave)="popupPreview.close()">
 | 
			
		||||
                <tr (click)="toggleSelected(document); $event.stopPropagation();" (mouseleave)="popupPreview.close()" class="data-row" [class.reveal]="reveal">
 | 
			
		||||
                    <td>
 | 
			
		||||
                    <div class="form-check m-0 ms-2 me-n2">
 | 
			
		||||
                        <input type="checkbox" class="form-check-input" id="{{document.id}}" [checked]="selectedDocuments.has(document.id)" (click)="toggleSelected(document); $event.stopPropagation();">
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
.data-row {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transition: opacity .2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.reveal {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
@ -69,6 +69,7 @@ describe('TrashComponent', () => {
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should call correct service method on reload', () => {
 | 
			
		||||
    jest.useFakeTimers()
 | 
			
		||||
    const trashSpy = jest.spyOn(trashService, 'getTrash')
 | 
			
		||||
    trashSpy.mockReturnValue(
 | 
			
		||||
      of({
 | 
			
		||||
@ -78,6 +79,7 @@ describe('TrashComponent', () => {
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
    component.reload()
 | 
			
		||||
    jest.advanceTimersByTime(100)
 | 
			
		||||
    expect(trashSpy).toHaveBeenCalled()
 | 
			
		||||
    expect(component.documentsInTrash).toEqual(documentsInTrash)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import { Document } from 'src/app/data/document'
 | 
			
		||||
import { ToastService } from 'src/app/services/toast.service'
 | 
			
		||||
import { TrashService } from 'src/app/services/trash.service'
 | 
			
		||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
 | 
			
		||||
import { Subject, takeUntil } from 'rxjs'
 | 
			
		||||
import { delay, Subject, takeUntil, tap } from 'rxjs'
 | 
			
		||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
 | 
			
		||||
import { Router } from '@angular/router'
 | 
			
		||||
@ -21,6 +21,7 @@ export class TrashComponent implements OnDestroy {
 | 
			
		||||
  public page: number = 1
 | 
			
		||||
  public totalDocuments: number
 | 
			
		||||
  public isLoading: boolean = false
 | 
			
		||||
  public reveal: boolean = false
 | 
			
		||||
  unsubscribeNotifier: Subject<void> = new Subject()
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
@ -40,12 +41,20 @@ export class TrashComponent implements OnDestroy {
 | 
			
		||||
 | 
			
		||||
  reload() {
 | 
			
		||||
    this.isLoading = true
 | 
			
		||||
    this.trashService.getTrash(this.page).subscribe((r) => {
 | 
			
		||||
      this.documentsInTrash = r.results
 | 
			
		||||
      this.totalDocuments = r.count
 | 
			
		||||
      this.isLoading = false
 | 
			
		||||
      this.selectedDocuments.clear()
 | 
			
		||||
    })
 | 
			
		||||
    this.trashService
 | 
			
		||||
      .getTrash(this.page)
 | 
			
		||||
      .pipe(
 | 
			
		||||
        tap((r) => {
 | 
			
		||||
          this.documentsInTrash = r.results
 | 
			
		||||
          this.totalDocuments = r.count
 | 
			
		||||
          this.selectedDocuments.clear()
 | 
			
		||||
        }),
 | 
			
		||||
        delay(100)
 | 
			
		||||
      )
 | 
			
		||||
      .subscribe(() => {
 | 
			
		||||
        this.reveal = true
 | 
			
		||||
        this.isLoading = false
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  delete(document: Document) {
 | 
			
		||||
 | 
			
		||||
@ -11,8 +11,19 @@
 | 
			
		||||
      >
 | 
			
		||||
      @if (savedViewService.loading) {
 | 
			
		||||
        <div class="col">
 | 
			
		||||
          <div class="spinner-border spinner-border-sm me-2" role="status"></div>
 | 
			
		||||
          <ng-container i18n>Loading...</ng-container>
 | 
			
		||||
          <div class="card shadow-sm bg-light">
 | 
			
		||||
            <div class="card-header">
 | 
			
		||||
              <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                <div class="d-flex">
 | 
			
		||||
                  <div class="ms-n2 me-1">
 | 
			
		||||
                    <i-bs name="grip-vertical"></i-bs>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <h6 class="card-title mb-0" i18n></h6>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-body"> </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,8 +9,9 @@
 | 
			
		||||
    <a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @if (documents.length && displayMode === DisplayMode.TABLE) {
 | 
			
		||||
    <table content class="table table-hover mb-0 mt-n2 align-middle">
 | 
			
		||||
  <div content class="wrapper" [class.reveal]="reveal">
 | 
			
		||||
  @if (displayMode === DisplayMode.TABLE) {
 | 
			
		||||
    <table class="table table-hover mb-0 mt-n2 align-middle">
 | 
			
		||||
      <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
          @for (field of displayFields; track field; let i = $index) {
 | 
			
		||||
@ -28,53 +29,59 @@
 | 
			
		||||
        </tr>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody>
 | 
			
		||||
        @for (doc of documents; track doc.id) {
 | 
			
		||||
        @for (doc of documents; track doc.id; let i = $index) {
 | 
			
		||||
          <tr>
 | 
			
		||||
            @for (field of displayFields; track field; let i = $index) {
 | 
			
		||||
              <td class="py-2 py-md-3 position-relative" [ngClass]="{ 'd-none d-md-table-cell': i > 1 }">
 | 
			
		||||
                @switch (field) {
 | 
			
		||||
                  @case (DisplayField.ADDED) {
 | 
			
		||||
                    <a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.added | customDate}}</a>
 | 
			
		||||
                  }
 | 
			
		||||
                  @case (DisplayField.CREATED) {
 | 
			
		||||
                    <a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.created_date | customDate}}</a>
 | 
			
		||||
                  }
 | 
			
		||||
                  @case (DisplayField.TITLE) {
 | 
			
		||||
                    <a routerLink="/documents/{{doc.id}}" title="Open document" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
 | 
			
		||||
                  }
 | 
			
		||||
                  @case (DisplayField.CORRESPONDENT) {
 | 
			
		||||
                    @if (doc.correspondent) {
 | 
			
		||||
                      <a class="btn-link text-dark text-decoration-none" type="button" (click)="clickCorrespondent(doc.correspondent, $event)" title="Filter by correspondent" i18n-title>{{(doc.correspondent$ | async)?.name}}</a>
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                  @case (DisplayField.TAGS) {
 | 
			
		||||
                    @for (t of doc.tags$ | async; track t) {
 | 
			
		||||
                      <pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t.id, $event)" [clickable]="true" linkTitle="Filter by tag" i18n-title></pngx-tag>
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                  @case (DisplayField.DOCUMENT_TYPE) {
 | 
			
		||||
                    @if (doc.document_type) {
 | 
			
		||||
                      <a class="btn-link text-dark text-decoration-none" type="button" (click)="clickDocType(doc.document_type, $event)" title="Filter by document type" i18n-title>{{(doc.document_type$ | async)?.name}}</a>
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                  @case (DisplayField.STORAGE_PATH) {
 | 
			
		||||
                    @if (doc.storage_path) {
 | 
			
		||||
                      <a class="btn-link text-dark text-decoration-none" type="button" (click)="clickStoragePath(doc.storage_path, $event)" title="Filter by storage path" i18n-title>{{(doc.storage_path$ | async)?.name}}</a>
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
                @if (field.startsWith(DisplayField.CUSTOM_FIELD)) {
 | 
			
		||||
                  <pngx-custom-field-display [document]="doc" [fieldDisplayKey]="field"></pngx-custom-field-display>
 | 
			
		||||
                }
 | 
			
		||||
                @if (i === displayFields.length - 1) {
 | 
			
		||||
                  <div class="btn-group position-absolute top-50 end-0 translate-middle-y" (mouseleave)="popupPreview.close()">
 | 
			
		||||
                    <pngx-preview-popup [document]="doc" linkClasses="btn px-4 btn-dark border-dark-subtle" #popupPreview>
 | 
			
		||||
                      <i-bs width="0.8em" height="0.8em" name="eye"></i-bs>
 | 
			
		||||
                    </pngx-preview-popup>
 | 
			
		||||
                    <a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
 | 
			
		||||
                      <i-bs width="0.8em" height="0.8em" name="download"></i-bs>
 | 
			
		||||
                    </a>
 | 
			
		||||
            @for (field of displayFields; track field; let j = $index) {
 | 
			
		||||
              <td class="py-2 py-md-3 position-relative" [ngClass]="{ 'd-none d-md-table-cell': j > 1 }">
 | 
			
		||||
                @if (loading && reveal) {
 | 
			
		||||
                  <div class="placeholder-glow text-start">
 | 
			
		||||
                    <span class="placeholder bg-secondary w-50" [ngStyle]="{ opacity: 1 - (i * 1/documents.length) }"></span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                } @else {
 | 
			
		||||
                  @switch (field) {
 | 
			
		||||
                    @case (DisplayField.ADDED) {
 | 
			
		||||
                      <a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.added | customDate}}</a>
 | 
			
		||||
                    }
 | 
			
		||||
                    @case (DisplayField.CREATED) {
 | 
			
		||||
                      <a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.created_date | customDate}}</a>
 | 
			
		||||
                    }
 | 
			
		||||
                    @case (DisplayField.TITLE) {
 | 
			
		||||
                      <a routerLink="/documents/{{doc.id}}" title="Open document" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
 | 
			
		||||
                    }
 | 
			
		||||
                    @case (DisplayField.CORRESPONDENT) {
 | 
			
		||||
                      @if (doc.correspondent) {
 | 
			
		||||
                        <a class="btn-link text-dark text-decoration-none" type="button" (click)="clickCorrespondent(doc.correspondent, $event)" title="Filter by correspondent" i18n-title>{{(doc.correspondent$ | async)?.name}}</a>
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                    @case (DisplayField.TAGS) {
 | 
			
		||||
                      @for (t of doc.tags$ | async; track t) {
 | 
			
		||||
                        <pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t.id, $event)" [clickable]="true" linkTitle="Filter by tag" i18n-title></pngx-tag>
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                    @case (DisplayField.DOCUMENT_TYPE) {
 | 
			
		||||
                      @if (doc.document_type) {
 | 
			
		||||
                        <a class="btn-link text-dark text-decoration-none" type="button" (click)="clickDocType(doc.document_type, $event)" title="Filter by document type" i18n-title>{{(doc.document_type$ | async)?.name}}</a>
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                    @case (DisplayField.STORAGE_PATH) {
 | 
			
		||||
                      @if (doc.storage_path) {
 | 
			
		||||
                        <a class="btn-link text-dark text-decoration-none" type="button" (click)="clickStoragePath(doc.storage_path, $event)" title="Filter by storage path" i18n-title>{{(doc.storage_path$ | async)?.name}}</a>
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                  @if (field.startsWith(DisplayField.CUSTOM_FIELD)) {
 | 
			
		||||
                    <pngx-custom-field-display [document]="doc" [fieldDisplayKey]="field"></pngx-custom-field-display>
 | 
			
		||||
                  }
 | 
			
		||||
                  @if (j === displayFields.length - 1) {
 | 
			
		||||
                    <div class="btn-group position-absolute top-50 end-0 translate-middle-y" (mouseleave)="popupPreview.close()">
 | 
			
		||||
                      <pngx-preview-popup [document]="doc" linkClasses="btn px-4 btn-dark border-dark-subtle" #popupPreview>
 | 
			
		||||
                        <i-bs width="0.8em" height="0.8em" name="eye"></i-bs>
 | 
			
		||||
                      </pngx-preview-popup>
 | 
			
		||||
                      <a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
 | 
			
		||||
                        <i-bs width="0.8em" height="0.8em" name="download"></i-bs>
 | 
			
		||||
                      </a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              </td>
 | 
			
		||||
            }
 | 
			
		||||
@ -82,13 +89,14 @@
 | 
			
		||||
        }
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
  } @else if (documents.length && displayMode === DisplayMode.SMALL_CARDS) {
 | 
			
		||||
    <div content class="row row-cols-paperless-cards my-n2">
 | 
			
		||||
      @for (d of documents; track d.id) {
 | 
			
		||||
  } @else if (displayMode === DisplayMode.SMALL_CARDS) {
 | 
			
		||||
    <div class="row row-cols-paperless-cards my-n2">
 | 
			
		||||
      @for (d of documents; track d.id; let i = $index) {
 | 
			
		||||
        <pngx-document-card-small
 | 
			
		||||
          class="p-0"
 | 
			
		||||
          [ngStyle]="{ opacity: !loading && reveal ? 1 : 1 - (i * 1/documents.length) }"
 | 
			
		||||
          (dblClickDocument)="openDocumentDetail(d)"
 | 
			
		||||
          [document]="d"
 | 
			
		||||
          [document]="!loading && reveal ? d : null"
 | 
			
		||||
          [displayFields]="displayFields"
 | 
			
		||||
          (clickTag)="clickTag($event)"
 | 
			
		||||
          (clickCorrespondent)="clickCorrespondent($event)"
 | 
			
		||||
@ -97,12 +105,13 @@
 | 
			
		||||
        </pngx-document-card-small>
 | 
			
		||||
      }
 | 
			
		||||
    </div>
 | 
			
		||||
  } @else if (documents.length && displayMode === DisplayMode.LARGE_CARDS) {
 | 
			
		||||
    <div content class="row my-n2">
 | 
			
		||||
      @for (d of documents; track d.id) {
 | 
			
		||||
  } @else if (displayMode === DisplayMode.LARGE_CARDS) {
 | 
			
		||||
    <div class="row my-n2">
 | 
			
		||||
      @for (d of documents; track d.id; let i = $index) {
 | 
			
		||||
        <pngx-document-card-large
 | 
			
		||||
          (dblClickDocument)="openDocumentDetail(d)"
 | 
			
		||||
          [document]="d"
 | 
			
		||||
          [document]="!loading && reveal ? d : null"
 | 
			
		||||
          [ngStyle]="{ opacity: !loading && reveal ? 1 : 1 - (i * 1/documents.length) }"
 | 
			
		||||
          [displayFields]="displayFields"
 | 
			
		||||
          (clickTag)="clickTag($event)"
 | 
			
		||||
          (clickCorrespondent)="clickCorrespondent($event)"
 | 
			
		||||
@ -113,8 +122,8 @@
 | 
			
		||||
      }
 | 
			
		||||
    </div>
 | 
			
		||||
  } @else {
 | 
			
		||||
    <p content i18n class="text-center text-muted mb-0 fst-italic">No documents</p>
 | 
			
		||||
    <p i18n class="text-center text-muted mb-0 fst-italic">No documents</p>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
</pngx-widget-frame>
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,17 @@
 | 
			
		||||
.wrapper {
 | 
			
		||||
  transition: all .3s ease-out;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  max-height: 0;
 | 
			
		||||
  opacity: .1;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.reveal {
 | 
			
		||||
  max-height: 1000px;
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  overflow: visible;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table {
 | 
			
		||||
  overflow-wrap: anywhere;
 | 
			
		||||
  table-layout: fixed;
 | 
			
		||||
 | 
			
		||||
@ -187,7 +187,7 @@ describe('SavedViewWidgetComponent', () => {
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should show a list of documents', () => {
 | 
			
		||||
  it('should show a list of documents', fakeAsync(() => {
 | 
			
		||||
    jest.spyOn(documentService, 'listFiltered').mockReturnValue(
 | 
			
		||||
      of({
 | 
			
		||||
        all: [2, 3],
 | 
			
		||||
@ -196,6 +196,7 @@ describe('SavedViewWidgetComponent', () => {
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
    component.ngOnInit()
 | 
			
		||||
    tick(500)
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    expect(fixture.debugElement.nativeElement.textContent).toContain('doc2')
 | 
			
		||||
    expect(fixture.debugElement.nativeElement.textContent).toContain('doc3')
 | 
			
		||||
@ -206,7 +207,7 @@ describe('SavedViewWidgetComponent', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      fixture.debugElement.queryAll(By.css('td a.btn'))[1].attributes['href']
 | 
			
		||||
    ).toEqual(component.getDownloadUrl(documentResults[0]))
 | 
			
		||||
  })
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  it('should call api endpoint and load results', () => {
 | 
			
		||||
    const listAllSpy = jest.spyOn(documentService, 'listFiltered')
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import {
 | 
			
		||||
  ViewChildren,
 | 
			
		||||
} from '@angular/core'
 | 
			
		||||
import { Router } from '@angular/router'
 | 
			
		||||
import { Subject, takeUntil } from 'rxjs'
 | 
			
		||||
import { delay, Subject, takeUntil, tap } from 'rxjs'
 | 
			
		||||
import {
 | 
			
		||||
  DEFAULT_DASHBOARD_DISPLAY_FIELDS,
 | 
			
		||||
  DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
 | 
			
		||||
@ -52,7 +52,8 @@ export class SavedViewWidgetComponent
 | 
			
		||||
  public DisplayField = DisplayField
 | 
			
		||||
  public CustomFieldDataType = CustomFieldDataType
 | 
			
		||||
 | 
			
		||||
  loading: boolean = true
 | 
			
		||||
  public loading: boolean = true
 | 
			
		||||
  public reveal: boolean = false
 | 
			
		||||
 | 
			
		||||
  private customFields: CustomField[] = []
 | 
			
		||||
 | 
			
		||||
@ -133,16 +134,22 @@ export class SavedViewWidgetComponent
 | 
			
		||||
    this.documentService
 | 
			
		||||
      .listFiltered(
 | 
			
		||||
        1,
 | 
			
		||||
        this.savedView.page_size ?? DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
 | 
			
		||||
        this.savedView?.page_size ?? DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
 | 
			
		||||
        this.savedView.sort_field,
 | 
			
		||||
        this.savedView.sort_reverse,
 | 
			
		||||
        this.savedView.filter_rules,
 | 
			
		||||
        { truncate_content: true }
 | 
			
		||||
      )
 | 
			
		||||
      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
      .pipe(
 | 
			
		||||
        takeUntil(this.unsubscribeNotifier),
 | 
			
		||||
        tap((result) => {
 | 
			
		||||
          this.reveal = true
 | 
			
		||||
          this.documents = result.results
 | 
			
		||||
        }),
 | 
			
		||||
        delay(500)
 | 
			
		||||
      )
 | 
			
		||||
      .subscribe((result) => {
 | 
			
		||||
        this.loading = false
 | 
			
		||||
        this.documents = result.results
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,25 +1,49 @@
 | 
			
		||||
<pngx-widget-frame title="Statistics" [loading]="loading" i18n-title>
 | 
			
		||||
  <ng-container content>
 | 
			
		||||
    <div class="list-group border-light">
 | 
			
		||||
      @if (statistics?.documents_inbox !== null) {
 | 
			
		||||
        <a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to inbox" i18n-title href="javascript:void(0)" (click)="goToInbox()">
 | 
			
		||||
          <ng-container i18n>Documents in inbox</ng-container>:
 | 
			
		||||
          <span class="badge rounded-pill" [class.bg-primary]="statistics?.documents_inbox > 0" [class.bg-muted]="statistics?.documents_inbox === 0">{{statistics?.documents_inbox}}</span>
 | 
			
		||||
        </a>
 | 
			
		||||
      }
 | 
			
		||||
      <a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to documents" i18n-title routerLink="/documents/">
 | 
			
		||||
        <ng-container i18n>Total documents</ng-container>:
 | 
			
		||||
        <span class="badge bg-primary rounded-pill">{{statistics?.documents_total}}</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <div class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documents/">
 | 
			
		||||
        <ng-container i18n>Total characters</ng-container>:
 | 
			
		||||
        <span class="badge bg-secondary text-light rounded-pill">{{statistics?.character_count | number}}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      @if (statistics?.current_asn) {
 | 
			
		||||
        <div class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documents/">
 | 
			
		||||
          <ng-container i18n>Current ASN</ng-container>:
 | 
			
		||||
          <span class="badge bg-secondary text-light rounded-pill">{{statistics?.current_asn}}</span>
 | 
			
		||||
    <div class="list-group border-light placeholder-glow">
 | 
			
		||||
      @if (loading) {
 | 
			
		||||
        <div class="list-group-item d-flex">
 | 
			
		||||
          <div class="placeholder w-50"></div>
 | 
			
		||||
          <span class="placeholder badge rounded-pill ms-auto" style="width: 25px;"> </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="list-group-item d-flex">
 | 
			
		||||
          <div class="placeholder w-25"></div>
 | 
			
		||||
          <span class="placeholder badge rounded-pill ms-auto" style="width: 25px;"> </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="list-group-item d-flex">
 | 
			
		||||
          <div class="placeholder w-25"></div>
 | 
			
		||||
          <span class="placeholder badge rounded-pill ms-auto" style="width: 25px;"> </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="list-group-item d-flex">
 | 
			
		||||
          <div class="placeholder w-25"></div>
 | 
			
		||||
          <span class="placeholder badge rounded-pill ms-auto" style="width: 25px;"> </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="list-group-item filetypes">
 | 
			
		||||
          <div class="placeholder w-100 d-block mb-2"></div>
 | 
			
		||||
          <div class="placeholder w-100 d-block mb-2"></div>
 | 
			
		||||
          <div class="placeholder w-100 d-block"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      } @else {
 | 
			
		||||
        @if (statistics?.documents_inbox !== null) {
 | 
			
		||||
          <a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to inbox" i18n-title href="javascript:void(0)" (click)="goToInbox()">
 | 
			
		||||
            <ng-container i18n>Documents in inbox</ng-container>:
 | 
			
		||||
            <span class="badge rounded-pill" [class.bg-primary]="statistics?.documents_inbox > 0" [class.bg-muted]="statistics?.documents_inbox === 0">{{statistics?.documents_inbox}}</span>
 | 
			
		||||
          </a>
 | 
			
		||||
        }
 | 
			
		||||
        <a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to documents" i18n-title routerLink="/documents/">
 | 
			
		||||
          <ng-container i18n>Total documents</ng-container>:
 | 
			
		||||
          <span class="badge bg-primary rounded-pill">{{statistics?.documents_total}}</span>
 | 
			
		||||
        </a>
 | 
			
		||||
        <div class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documents/">
 | 
			
		||||
          <ng-container i18n>Total characters</ng-container>:
 | 
			
		||||
          <span class="badge bg-secondary text-light rounded-pill">{{statistics?.character_count | number}}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        @if (statistics?.current_asn) {
 | 
			
		||||
          <div class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documents/">
 | 
			
		||||
            <ng-container i18n>Current ASN</ng-container>:
 | 
			
		||||
            <span class="badge bg-secondary text-light rounded-pill">{{statistics?.current_asn}}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      @if (statistics?.document_file_type_counts?.length > 1) {
 | 
			
		||||
        <div class="list-group-item filetypes">
 | 
			
		||||
@ -59,6 +83,11 @@
 | 
			
		||||
 | 
			
		||||
    <div class="list-group border-light mt-3">
 | 
			
		||||
      <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
 | 
			
		||||
        @if (loading) {
 | 
			
		||||
          <div class="placeholder-glow list-group-item">
 | 
			
		||||
            <span class="placeholder w-100"></span>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @if (statistics?.tag_count > 0) {
 | 
			
		||||
          <a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/tags/">
 | 
			
		||||
            <ng-container i18n>Tags</ng-container>:
 | 
			
		||||
@ -67,6 +96,11 @@
 | 
			
		||||
        }
 | 
			
		||||
      </ng-container>
 | 
			
		||||
      <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
 | 
			
		||||
        @if (loading) {
 | 
			
		||||
          <div class="placeholder-glow list-group-item">
 | 
			
		||||
            <span class="placeholder w-100"></span>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @if (statistics?.correspondent_count > 0) {
 | 
			
		||||
          <a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/correspondents/">
 | 
			
		||||
            <ng-container i18n>Correspondents</ng-container>:
 | 
			
		||||
@ -75,6 +109,11 @@
 | 
			
		||||
        }
 | 
			
		||||
      </ng-container>
 | 
			
		||||
      <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
 | 
			
		||||
        @if (loading) {
 | 
			
		||||
          <div class="placeholder-glow list-group-item">
 | 
			
		||||
            <span class="placeholder w-100"></span>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @if (statistics?.document_type_count > 0) {
 | 
			
		||||
          <a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documenttypes/">
 | 
			
		||||
            <ng-container i18n>Document Types</ng-container>:
 | 
			
		||||
@ -83,6 +122,11 @@
 | 
			
		||||
        }
 | 
			
		||||
      </ng-container>
 | 
			
		||||
      <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
 | 
			
		||||
        @if (loading) {
 | 
			
		||||
          <div class="placeholder-glow list-group-item">
 | 
			
		||||
            <span class="placeholder w-100"></span>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @if (statistics?.storage_path_count > 0) {
 | 
			
		||||
          <a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/storagepaths/">
 | 
			
		||||
            <ng-container i18n>Storage Paths</ng-container>:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
<div class="card shadow-sm bg-light" cdkDrag [cdkDragDisabled]="!draggable" cdkDragPreviewContainer="parent">
 | 
			
		||||
<div class="card shadow-sm bg-light fade" [class.reveal]="reveal" cdkDrag [cdkDragDisabled]="!draggable" cdkDragPreviewContainer="parent">
 | 
			
		||||
  <div class="card-header">
 | 
			
		||||
    <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
      <div class="d-flex">
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,12 @@
 | 
			
		||||
i-bs {
 | 
			
		||||
    cursor: move;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transition: opacity .2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.reveal {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@ describe('WidgetFrameComponent', () => {
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(WidgetFrameComponent)
 | 
			
		||||
    component = fixture.componentInstance
 | 
			
		||||
    jest.useFakeTimers()
 | 
			
		||||
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
  })
 | 
			
		||||
@ -51,4 +52,10 @@ describe('WidgetFrameComponent', () => {
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    expect(fixture.debugElement.query(By.css('.spinner-border'))).not.toBeNull()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should reveal', () => {
 | 
			
		||||
    expect(component.reveal).toBeFalsy()
 | 
			
		||||
    jest.advanceTimersByTime(100)
 | 
			
		||||
    expect(component.reveal).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,11 @@
 | 
			
		||||
import { Component, Input } from '@angular/core'
 | 
			
		||||
import { AfterViewInit, Component, Input } from '@angular/core'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'pngx-widget-frame',
 | 
			
		||||
  templateUrl: './widget-frame.component.html',
 | 
			
		||||
  styleUrls: ['./widget-frame.component.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class WidgetFrameComponent {
 | 
			
		||||
export class WidgetFrameComponent implements AfterViewInit {
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
  @Input()
 | 
			
		||||
@ -16,4 +16,12 @@ export class WidgetFrameComponent {
 | 
			
		||||
 | 
			
		||||
  @Input()
 | 
			
		||||
  draggable: any
 | 
			
		||||
 | 
			
		||||
  public reveal: boolean = false
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit(): void {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      this.reveal = true
 | 
			
		||||
    }, 100)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,70 +1,92 @@
 | 
			
		||||
<div class="card mb-3 shadow-sm bg-light" [class.card-selected]="selected" [class.document-card]="selectable" (mouseleave)="mouseLeaveCard()">
 | 
			
		||||
<div class="card document-card-large mb-3 shadow-sm bg-light placeholder-glow" [class.card-selected]="selected" [class.document-card]="selectable" (mouseleave)="mouseLeaveCard()">
 | 
			
		||||
  <div class="row g-0">
 | 
			
		||||
    <div class="col-md-2 doc-img-container rounded-start" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit()">
 | 
			
		||||
      <img [src]="getThumbUrl()" class="card-img doc-img border-end rounded-start" [class.inverted]="getIsThumbInverted()">
 | 
			
		||||
      @if (document) {
 | 
			
		||||
        <img [src]="getThumbUrl()" class="card-img doc-img border-end rounded-start" [class.inverted]="getIsThumbInverted()">
 | 
			
		||||
 | 
			
		||||
      <div class="border-end border-bottom bg-light document-card-check">
 | 
			
		||||
        <div class="form-check">
 | 
			
		||||
          <input type="checkbox" class="form-check-input" id="smallCardCheck{{document.id}}" [checked]="selected" (click)="this.toggleSelected.emit($event)">
 | 
			
		||||
          <label class="form-check-label" for="smallCardCheck{{document.id}}"></label>
 | 
			
		||||
        <div class="border-end border-bottom bg-light document-card-check">
 | 
			
		||||
          <div class="form-check">
 | 
			
		||||
            <input type="checkbox" class="form-check-input" id="smallCardCheck{{document.id}}" [checked]="selected" (click)="this.toggleSelected.emit($event)">
 | 
			
		||||
            <label class="form-check-label" for="smallCardCheck{{document.id}}"></label>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      } @else {
 | 
			
		||||
        <div class="placeholder bg-secondary w-100 card-img doc-img border-end rounded-start"></div>
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card-body">
 | 
			
		||||
        <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
          <h5 class="card-title">
 | 
			
		||||
            @if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
 | 
			
		||||
              @if (clickCorrespondent.observers.length ) {
 | 
			
		||||
                <a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>
 | 
			
		||||
              } @else {
 | 
			
		||||
                {{(document.correspondent$ | async)?.name}}
 | 
			
		||||
          <h5 class="card-title w-100">
 | 
			
		||||
            @if (document) {
 | 
			
		||||
              @if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
 | 
			
		||||
                @if (clickCorrespondent.observers.length ) {
 | 
			
		||||
                  <a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>
 | 
			
		||||
                } @else {
 | 
			
		||||
                  {{(document.correspondent$ | async)?.name}}
 | 
			
		||||
                }
 | 
			
		||||
                @if (displayFields.includes(DisplayField.TITLE)) {:}
 | 
			
		||||
              }
 | 
			
		||||
              @if (displayFields.includes(DisplayField.TITLE)) {:}
 | 
			
		||||
            }
 | 
			
		||||
            @if (displayFields.includes(DisplayField.TITLE)) {
 | 
			
		||||
              {{document.title | documentTitle}}
 | 
			
		||||
            }
 | 
			
		||||
            @if (displayFields.includes(DisplayField.TAGS)) {
 | 
			
		||||
              @for (t of document.tags$ | async; track t) {
 | 
			
		||||
                <pngx-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle class="ms-1" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="clickTag.observers.length"></pngx-tag>
 | 
			
		||||
              @if (displayFields.includes(DisplayField.TITLE)) {
 | 
			
		||||
                {{document.title | documentTitle}}
 | 
			
		||||
              }
 | 
			
		||||
              @if (displayFields.includes(DisplayField.TAGS)) {
 | 
			
		||||
                @for (t of document.tags$ | async; track t) {
 | 
			
		||||
                  <pngx-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle class="ms-1" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="clickTag.observers.length"></pngx-tag>
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            } @else {
 | 
			
		||||
              <div class="placeholder bg-secondary w-75 mb-3"> </div>
 | 
			
		||||
            }
 | 
			
		||||
          </h5>
 | 
			
		||||
        </div>
 | 
			
		||||
        <p class="card-text">
 | 
			
		||||
          @if (document.__search_hit__ && document.__search_hit__.highlights) {
 | 
			
		||||
            <span [innerHtml]="document.__search_hit__.highlights"></span>
 | 
			
		||||
          }
 | 
			
		||||
          @for (highlight of searchNoteHighlights; track highlight) {
 | 
			
		||||
            <span class="d-block">
 | 
			
		||||
              <i-bs name="chat-left-text"></i-bs>
 | 
			
		||||
              <span [innerHtml]="highlight"></span>
 | 
			
		||||
            </span>
 | 
			
		||||
          }
 | 
			
		||||
          @if (!document.__search_hit__?.score) {
 | 
			
		||||
            <span class="result-content">{{contentTrimmed}}</span>
 | 
			
		||||
          @if (document) {
 | 
			
		||||
            @if (document.__search_hit__ && document.__search_hit__.highlights) {
 | 
			
		||||
              <span [innerHtml]="document.__search_hit__.highlights"></span>
 | 
			
		||||
            }
 | 
			
		||||
            @for (highlight of searchNoteHighlights; track highlight) {
 | 
			
		||||
              <span class="d-block">
 | 
			
		||||
                <i-bs name="chat-left-text"></i-bs>
 | 
			
		||||
                <span [innerHtml]="highlight"></span>
 | 
			
		||||
              </span>
 | 
			
		||||
            }
 | 
			
		||||
            @if (!document.__search_hit__?.score) {
 | 
			
		||||
              <span class="result-content">{{contentTrimmed}}</span>
 | 
			
		||||
            }
 | 
			
		||||
          } @else {
 | 
			
		||||
            <div class="placeholder bg-secondary w-100 d-block mb-2"></div>
 | 
			
		||||
            <div class="placeholder bg-secondary w-75 d-block mb-2"></div>
 | 
			
		||||
            <div class="placeholder bg-secondary w-25 d-block"></div>
 | 
			
		||||
          }
 | 
			
		||||
        </p>
 | 
			
		||||
 | 
			
		||||
        <div class="d-flex flex-column flex-md-row align-items-md-center">
 | 
			
		||||
          <div class="btn-group">
 | 
			
		||||
            <a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()">
 | 
			
		||||
              <i-bs name="diagram-3"></i-bs> <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" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
 | 
			
		||||
              <i-bs name="file-earmark-richtext"></i-bs> <span class="d-none d-md-inline" i18n>Open</span>
 | 
			
		||||
            </a>
 | 
			
		||||
            <pngx-preview-popup [document]="document" #popupPreview>
 | 
			
		||||
              <i-bs name="eye"></i-bs> <span class="d-none d-md-inline" i18n>View</span>
 | 
			
		||||
            </pngx-preview-popup>
 | 
			
		||||
            <a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
 | 
			
		||||
              <i-bs name="download"></i-bs> <span class="d-none d-md-inline" i18n>Download</span>
 | 
			
		||||
            @if (document) {
 | 
			
		||||
              <a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()">
 | 
			
		||||
                <i-bs name="diagram-3"></i-bs> <span class="d-none d-md-inline" i18n>More like this</span>
 | 
			
		||||
              </a>
 | 
			
		||||
            </div>
 | 
			
		||||
              <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
 | 
			
		||||
                <i-bs name="file-earmark-richtext"></i-bs> <span class="d-none d-md-inline" i18n>Open</span>
 | 
			
		||||
              </a>
 | 
			
		||||
              <pngx-preview-popup [document]="document" #popupPreview>
 | 
			
		||||
                <i-bs name="eye"></i-bs> <span class="d-none d-md-inline" i18n>View</span>
 | 
			
		||||
              </pngx-preview-popup>
 | 
			
		||||
              <a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
 | 
			
		||||
                <i-bs name="download"></i-bs> <span class="d-none d-md-inline" i18n>Download</span>
 | 
			
		||||
              </a>
 | 
			
		||||
            } @else {
 | 
			
		||||
              <div class="placeholder btn btn-sm btn-outline-secondary bg-secondary" style="width: 60px;"> </div>
 | 
			
		||||
              <div class="placeholder btn btn-sm btn-outline-secondary bg-secondary" style="width: 60px;"> </div>
 | 
			
		||||
              <div class="placeholder btn btn-sm btn-outline-secondary bg-secondary" style="width: 60px;"> </div>
 | 
			
		||||
              <div class="placeholder btn btn-sm btn-outline-secondary bg-secondary" style="width: 60px;"> </div>
 | 
			
		||||
            }
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
            <div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
 | 
			
		||||
          <div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
 | 
			
		||||
            @if (document) {
 | 
			
		||||
              @if (displayFields.includes(DisplayField.NOTES) && notesEnabled && document.notes.length) {
 | 
			
		||||
                <button routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="View notes" i18n-title>
 | 
			
		||||
                  <i-bs width=".9em" height=".9em" class="me-2 text-muted" name="chat-left-text"></i-bs><small>{{document.notes.length}} Notes</small>
 | 
			
		||||
@ -138,9 +160,16 @@
 | 
			
		||||
                  </div>
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            </div>
 | 
			
		||||
            } @else {
 | 
			
		||||
              <div class="placeholder list-group-item bg-secondary w-25"> </div>
 | 
			
		||||
              <div class="placeholder list-group-item bg-secondary w-25"> </div>
 | 
			
		||||
              <div class="placeholder list-group-item bg-secondary w-25"> </div>
 | 
			
		||||
              <div class="placeholder list-group-item bg-secondary w-25"> </div>
 | 
			
		||||
              <div class="placeholder list-group-item bg-secondary w-25"> </div>
 | 
			
		||||
            }
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,20 @@
 | 
			
		||||
<div class="col p-2 h-100">
 | 
			
		||||
  <div class="card h-100 shadow-sm document-card" [class.card-selected]="selected" (mouseleave)="mouseLeaveCard()">
 | 
			
		||||
  <div class="card h-100 shadow-sm document-card" [class.placeholder-glow]="!document" [class.card-selected]="selected" (mouseleave)="mouseLeaveCard()">
 | 
			
		||||
    <div class="border-bottom doc-img-container rounded-top" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit(this)">
 | 
			
		||||
      <img class="card-img doc-img" [class.inverted]="getIsThumbInverted()" [src]="getThumbUrl()">
 | 
			
		||||
      @if (document) {
 | 
			
		||||
        <img class="card-img doc-img" [class.inverted]="getIsThumbInverted()" [src]="getThumbUrl()">
 | 
			
		||||
 | 
			
		||||
      <div class="border-end border-bottom bg-light py-1 px-2 document-card-check">
 | 
			
		||||
        <div class="form-check">
 | 
			
		||||
          <input type="checkbox" class="form-check-input" id="smallCardCheck{{document.id}}" [checked]="selected" (click)="this.toggleSelected.emit($event)">
 | 
			
		||||
          <label class="form-check-label" for="smallCardCheck{{document.id}}"></label>
 | 
			
		||||
        <div class="border-end border-bottom bg-light py-1 px-2 document-card-check">
 | 
			
		||||
          <div class="form-check">
 | 
			
		||||
            <input type="checkbox" class="form-check-input" id="smallCardCheck{{document.id}}" [checked]="selected" (click)="this.toggleSelected.emit($event)">
 | 
			
		||||
            <label class="form-check-label" for="smallCardCheck{{document.id}}"></label>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      } @else {
 | 
			
		||||
        <div class="placeholder bg-secondary w-100 card-img doc-img"></div>
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      @if (displayFields?.includes(DisplayField.TAGS)) {
 | 
			
		||||
      @if (document && displayFields?.includes(DisplayField.TAGS)) {
 | 
			
		||||
        <div class="tags d-flex flex-column text-end position-absolute me-1 fs-6">
 | 
			
		||||
          @for (t of getTagsLimited$() | async; track t) {
 | 
			
		||||
            <pngx-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag>
 | 
			
		||||
@ -24,7 +28,7 @@
 | 
			
		||||
      }
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    @if (displayFields.includes(DisplayField.NOTES) && notesEnabled && document.notes.length) {
 | 
			
		||||
    @if (document && displayFields.includes(DisplayField.NOTES) && notesEnabled && document.notes.length) {
 | 
			
		||||
      <a routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1">
 | 
			
		||||
        <span class="badge rounded-pill bg-light border text-primary">
 | 
			
		||||
          <i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs>
 | 
			
		||||
@ -34,107 +38,122 @@
 | 
			
		||||
 | 
			
		||||
    <div class="card-body bg-light p-2">
 | 
			
		||||
      <p class="card-text">
 | 
			
		||||
        @if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
 | 
			
		||||
          <a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name ?? privateName}}</a>
 | 
			
		||||
          @if (displayFields.includes(DisplayField.TITLE)) {:}
 | 
			
		||||
        }
 | 
			
		||||
        @if (displayFields.includes(DisplayField.TITLE)) {
 | 
			
		||||
          {{document.title | documentTitle}}
 | 
			
		||||
        @if (document) {
 | 
			
		||||
          @if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
 | 
			
		||||
            <a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name ?? privateName}}</a>
 | 
			
		||||
            @if (displayFields.includes(DisplayField.TITLE)) {:}
 | 
			
		||||
          }
 | 
			
		||||
          @if (displayFields.includes(DisplayField.TITLE)) {
 | 
			
		||||
            {{document.title | documentTitle}}
 | 
			
		||||
          }
 | 
			
		||||
        } @else {
 | 
			
		||||
          <div class="placeholder bg-secondary w-100"></div>
 | 
			
		||||
          <div class="placeholder bg-secondary w-50"></div>
 | 
			
		||||
        }
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="card-footer pt-0 pb-2 px-2">
 | 
			
		||||
      <div class="list-group list-group-flush border-0 pt-1 pb-2 card-info">
 | 
			
		||||
        @if (displayFields.includes(DisplayField.DOCUMENT_TYPE) && document.document_type) {
 | 
			
		||||
          <button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title
 | 
			
		||||
            (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
 | 
			
		||||
            <i-bs width="1em" height="1em" class="me-2 text-muted" name="file-earmark"></i-bs>
 | 
			
		||||
            <small>{{(document.document_type$ | async)?.name ?? privateName}}</small>
 | 
			
		||||
          </button>
 | 
			
		||||
        }
 | 
			
		||||
        @if (displayFields.includes(DisplayField.STORAGE_PATH) && document.storage_path) {
 | 
			
		||||
          <button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title
 | 
			
		||||
            (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
 | 
			
		||||
            <i-bs width="1em" height="1em" class="me-2 text-muted" name="folder"></i-bs>
 | 
			
		||||
            <small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
 | 
			
		||||
          </button>
 | 
			
		||||
        }
 | 
			
		||||
        @if (displayFields.includes(DisplayField.CREATED)) {
 | 
			
		||||
          <div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
 | 
			
		||||
            <ng-template #dateTooltip>
 | 
			
		||||
              <div class="d-flex flex-column text-light">
 | 
			
		||||
                <span i18n>Created: {{ document.created_date | customDate }}</span>
 | 
			
		||||
                <span i18n>Added: {{ document.added | customDate }}</span>
 | 
			
		||||
                <span i18n>Modified: {{ document.modified | customDate }}</span>
 | 
			
		||||
        @if (document) {
 | 
			
		||||
          @if (displayFields.includes(DisplayField.DOCUMENT_TYPE) && document.document_type) {
 | 
			
		||||
            <button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title
 | 
			
		||||
              (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
 | 
			
		||||
              <i-bs width="1em" height="1em" class="me-2 text-muted" name="file-earmark"></i-bs>
 | 
			
		||||
              <small>{{(document.document_type$ | async)?.name ?? privateName}}</small>
 | 
			
		||||
            </button>
 | 
			
		||||
          }
 | 
			
		||||
          @if (displayFields.includes(DisplayField.STORAGE_PATH) && document.storage_path) {
 | 
			
		||||
            <button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title
 | 
			
		||||
              (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
 | 
			
		||||
              <i-bs width="1em" height="1em" class="me-2 text-muted" name="folder"></i-bs>
 | 
			
		||||
              <small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
 | 
			
		||||
            </button>
 | 
			
		||||
          }
 | 
			
		||||
          @if (displayFields.includes(DisplayField.CREATED)) {
 | 
			
		||||
            <div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
 | 
			
		||||
              <ng-template #dateTooltip>
 | 
			
		||||
                <div class="d-flex flex-column text-light">
 | 
			
		||||
                  <span i18n>Created: {{ document.created_date | customDate }}</span>
 | 
			
		||||
                  <span i18n>Added: {{ document.added | customDate }}</span>
 | 
			
		||||
                  <span i18n>Modified: {{ document.modified | customDate }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
              <div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
 | 
			
		||||
                <i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
 | 
			
		||||
                <small>{{document.created_date | customDate:'mediumDate'}}</small>
 | 
			
		||||
              </div>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
            <div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
 | 
			
		||||
              <i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
 | 
			
		||||
              <small>{{document.created_date | customDate:'mediumDate'}}</small>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @if (displayFields.includes(DisplayField.ADDED)) {
 | 
			
		||||
          <div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
 | 
			
		||||
            <ng-template #dateTooltip>
 | 
			
		||||
              <div class="d-flex flex-column text-light">
 | 
			
		||||
                <span i18n>Created: {{ document.created | customDate }}</span>
 | 
			
		||||
                <span i18n>Added: {{ document.added | customDate }}</span>
 | 
			
		||||
                <span i18n>Modified: {{ document.modified | customDate }}</span>
 | 
			
		||||
              </div>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
            <div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
 | 
			
		||||
              <i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
 | 
			
		||||
              <small>{{document.added | customDate:'mediumDate'}}</small>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @if (displayFields.includes(DisplayField.PAGE_COUNT) && document.page_count) {
 | 
			
		||||
          <div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
 | 
			
		||||
            <div class="ps-0 p-1" placement="top">
 | 
			
		||||
              <i-bs width="1em" height="1em" class="me-2 text-muted" name="files"></i-bs>
 | 
			
		||||
              <small i18n>{document.page_count, plural, =1 {1 page} other {{{document.page_count}} pages}}</small>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @if (displayFields.includes(DisplayField.ASN) && document.archive_serial_number | isNumber) {
 | 
			
		||||
          <div class="ps-0 p-1">
 | 
			
		||||
            <i-bs width="1em" height="1em" class="me-2 text-muted" name="upc-scan"></i-bs>
 | 
			
		||||
            <small>#{{document.archive_serial_number}}</small>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
 | 
			
		||||
          <div class="ps-0 p-1">
 | 
			
		||||
            <i-bs width="1em" height="1em" class="me-2 text-muted" name="person-fill-lock"></i-bs>
 | 
			
		||||
            <small>{{document.owner | username}}</small>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @if (displayFields.includes(DisplayField.SHARED) && document.is_shared_by_requester) {
 | 
			
		||||
          <div class="ps-0 p-1">
 | 
			
		||||
            <i-bs width="1em" height="1em" class="me-2 text-muted" name="people-fill"></i-bs>
 | 
			
		||||
            <small i18n>Shared</small>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
        @for (field of document.custom_fields; track field.field) {
 | 
			
		||||
          @if (displayFields.includes(DisplayField.CUSTOM_FIELD + field.field)) {
 | 
			
		||||
            <div class="ps-0 p-1 d-flex align-items-center overflow-hidden">
 | 
			
		||||
              <i-bs width="1em" height="1em" class="me-2 text-muted" name="ui-radios"></i-bs>
 | 
			
		||||
              <small><pngx-custom-field-display [document]="document" [fieldId]="field.field" showNameIfEmpty="true"></pngx-custom-field-display></small>
 | 
			
		||||
            </div>
 | 
			
		||||
          }
 | 
			
		||||
          @if (displayFields.includes(DisplayField.ADDED)) {
 | 
			
		||||
            <div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
 | 
			
		||||
              <ng-template #dateTooltip>
 | 
			
		||||
                <div class="d-flex flex-column text-light">
 | 
			
		||||
                  <span i18n>Created: {{ document.created | customDate }}</span>
 | 
			
		||||
                  <span i18n>Added: {{ document.added | customDate }}</span>
 | 
			
		||||
                  <span i18n>Modified: {{ document.modified | customDate }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
              <div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
 | 
			
		||||
                <i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
 | 
			
		||||
                <small>{{document.added | customDate:'mediumDate'}}</small>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          }
 | 
			
		||||
          @if (displayFields.includes(DisplayField.PAGE_COUNT) && document.page_count) {
 | 
			
		||||
            <div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
 | 
			
		||||
              <div class="ps-0 p-1" placement="top">
 | 
			
		||||
                <i-bs width="1em" height="1em" class="me-2 text-muted" name="files"></i-bs>
 | 
			
		||||
                <small i18n>{document.page_count, plural, =1 {1 page} other {{{document.page_count}} pages}}</small>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          }
 | 
			
		||||
          @if (displayFields.includes(DisplayField.ASN) && document.archive_serial_number | isNumber) {
 | 
			
		||||
            <div class="ps-0 p-1">
 | 
			
		||||
              <i-bs width="1em" height="1em" class="me-2 text-muted" name="upc-scan"></i-bs>
 | 
			
		||||
              <small>#{{document.archive_serial_number}}</small>
 | 
			
		||||
            </div>
 | 
			
		||||
          }
 | 
			
		||||
          @if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
 | 
			
		||||
            <div class="ps-0 p-1">
 | 
			
		||||
              <i-bs width="1em" height="1em" class="me-2 text-muted" name="person-fill-lock"></i-bs>
 | 
			
		||||
              <small>{{document.owner | username}}</small>
 | 
			
		||||
            </div>
 | 
			
		||||
          }
 | 
			
		||||
          @if (displayFields.includes(DisplayField.SHARED) && document.is_shared_by_requester) {
 | 
			
		||||
            <div class="ps-0 p-1">
 | 
			
		||||
              <i-bs width="1em" height="1em" class="me-2 text-muted" name="people-fill"></i-bs>
 | 
			
		||||
              <small i18n>Shared</small>
 | 
			
		||||
            </div>
 | 
			
		||||
          }
 | 
			
		||||
          @for (field of document.custom_fields; track field.field) {
 | 
			
		||||
            @if (displayFields.includes(DisplayField.CUSTOM_FIELD + field.field)) {
 | 
			
		||||
              <div class="ps-0 p-1 d-flex align-items-center overflow-hidden">
 | 
			
		||||
                <i-bs width="1em" height="1em" class="me-2 text-muted" name="ui-radios"></i-bs>
 | 
			
		||||
                <small><pngx-custom-field-display [document]="document" [fieldId]="field.field" showNameIfEmpty="true"></pngx-custom-field-display></small>
 | 
			
		||||
              </div>
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        } @else {
 | 
			
		||||
          <div class="placeholder bg-secondary w-100"></div>
 | 
			
		||||
        }
 | 
			
		||||
      </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="Open" i18n-title *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n-title>
 | 
			
		||||
            <i-bs name="file-earmark-richtext"></i-bs>
 | 
			
		||||
          </a>
 | 
			
		||||
          <pngx-preview-popup [document]="document" #popupPreview>
 | 
			
		||||
            <i-bs name="eye"></i-bs>
 | 
			
		||||
          </pngx-preview-popup>
 | 
			
		||||
          <a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" i18n-title (click)="$event.stopPropagation()">
 | 
			
		||||
            <i-bs name="download"></i-bs>
 | 
			
		||||
          </a>
 | 
			
		||||
          @if (document) {
 | 
			
		||||
            <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Open" i18n-title *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n-title>
 | 
			
		||||
              <i-bs name="file-earmark-richtext"></i-bs>
 | 
			
		||||
            </a>
 | 
			
		||||
            <pngx-preview-popup [document]="document" #popupPreview>
 | 
			
		||||
              <i-bs name="eye"></i-bs>
 | 
			
		||||
            </pngx-preview-popup>
 | 
			
		||||
            <a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" i18n-title (click)="$event.stopPropagation()">
 | 
			
		||||
              <i-bs name="download"></i-bs>
 | 
			
		||||
            </a>
 | 
			
		||||
          } @else {
 | 
			
		||||
            <button class="btn btn-sm btn-outline-secondary placeholder bg-secondary"></button>
 | 
			
		||||
            <button class="btn btn-sm btn-outline-secondary placeholder bg-secondary"></button>
 | 
			
		||||
            <button class="btn btn-sm btn-outline-secondary placeholder bg-secondary"></button>
 | 
			
		||||
          }
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,6 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
 | 
			
		||||
import { ToastService } from 'src/app/services/toast.service'
 | 
			
		||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
 | 
			
		||||
import { ManagementListComponent } from '../management-list/management-list.component'
 | 
			
		||||
import { takeUntil } from 'rxjs'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'pngx-correspondent-list',
 | 
			
		||||
@ -65,24 +64,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Correspo
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public reloadData(): void {
 | 
			
		||||
    this.isLoading = true
 | 
			
		||||
    this.clearSelection()
 | 
			
		||||
    this.service
 | 
			
		||||
      .listFiltered(
 | 
			
		||||
        this.page,
 | 
			
		||||
        null,
 | 
			
		||||
        this.sortField,
 | 
			
		||||
        this.sortReverse,
 | 
			
		||||
        this._nameFilter,
 | 
			
		||||
        true,
 | 
			
		||||
        { last_correspondence: true }
 | 
			
		||||
      )
 | 
			
		||||
      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
      .subscribe((c) => {
 | 
			
		||||
        this.data = c.results
 | 
			
		||||
        this.collectionSize = c.count
 | 
			
		||||
        this.isLoading = false
 | 
			
		||||
      })
 | 
			
		||||
    super.reloadData({ last_correspondence: true })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getDeleteMessage(object: Correspondent) {
 | 
			
		||||
 | 
			
		||||
@ -13,16 +13,23 @@
 | 
			
		||||
<ul class="list-group">
 | 
			
		||||
 | 
			
		||||
  <li class="list-group-item">
 | 
			
		||||
    <div class="row">
 | 
			
		||||
    <div class="row reveal">
 | 
			
		||||
      <div class="col" i18n>Name</div>
 | 
			
		||||
      <div class="col" i18n>Data Type</div>
 | 
			
		||||
      <div class="col" i18n>Actions</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </li>
 | 
			
		||||
 | 
			
		||||
  @if (loading) {
 | 
			
		||||
    <li class="list-group-item">
 | 
			
		||||
      <div class="spinner-border spinner-border-sm me-2" role="status"></div>
 | 
			
		||||
      <ng-container i18n>Loading...</ng-container>
 | 
			
		||||
    </li>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @for (field of fields; track field) {
 | 
			
		||||
    <li class="list-group-item">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
      <div class="row" [class.reveal]="reveal">
 | 
			
		||||
        <div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div>
 | 
			
		||||
        <div class="col d-flex align-items-center">{{getDataType(field)}}</div>
 | 
			
		||||
        <div class="col">
 | 
			
		||||
@ -59,7 +66,7 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </li>
 | 
			
		||||
  }
 | 
			
		||||
  @if (fields.length === 0) {
 | 
			
		||||
  @if (!loading && fields.length === 0) {
 | 
			
		||||
    <li class="list-group-item" i18n>No fields defined.</li>
 | 
			
		||||
  }
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
@ -2,3 +2,12 @@
 | 
			
		||||
.d-block.d-sm-none .dropdown-toggle::after {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-group-item .row {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transition: opacity .2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-group-item .reveal {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -95,6 +95,8 @@ describe('CustomFieldsComponent', () => {
 | 
			
		||||
    fixture = TestBed.createComponent(CustomFieldsComponent)
 | 
			
		||||
    component = fixture.componentInstance
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    jest.useFakeTimers()
 | 
			
		||||
    jest.advanceTimersByTime(100)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support create, show notification on error / success', () => {
 | 
			
		||||
@ -119,6 +121,7 @@ describe('CustomFieldsComponent', () => {
 | 
			
		||||
    editDialog.succeeded.emit(fields[0])
 | 
			
		||||
    expect(toastInfoSpy).toHaveBeenCalled()
 | 
			
		||||
    expect(reloadSpy).toHaveBeenCalled()
 | 
			
		||||
    jest.advanceTimersByTime(100)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support edit, show notification on error / success', () => {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core'
 | 
			
		||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { Subject, takeUntil } from 'rxjs'
 | 
			
		||||
import { delay, Subject, takeUntil, tap } from 'rxjs'
 | 
			
		||||
import { DATA_TYPE_LABELS, CustomField } from 'src/app/data/custom-field'
 | 
			
		||||
import { PermissionsService } from 'src/app/services/permissions.service'
 | 
			
		||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
 | 
			
		||||
@ -28,6 +28,9 @@ export class CustomFieldsComponent
 | 
			
		||||
{
 | 
			
		||||
  public fields: CustomField[] = []
 | 
			
		||||
 | 
			
		||||
  public loading: boolean = true
 | 
			
		||||
  public reveal: boolean = false
 | 
			
		||||
 | 
			
		||||
  private unsubscribeNotifier: Subject<any> = new Subject()
 | 
			
		||||
  constructor(
 | 
			
		||||
    private customFieldsService: CustomFieldsService,
 | 
			
		||||
@ -47,9 +50,16 @@ export class CustomFieldsComponent
 | 
			
		||||
  reload() {
 | 
			
		||||
    this.customFieldsService
 | 
			
		||||
      .listAll()
 | 
			
		||||
      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
      .subscribe((r) => {
 | 
			
		||||
        this.fields = r.results
 | 
			
		||||
      .pipe(
 | 
			
		||||
        takeUntil(this.unsubscribeNotifier),
 | 
			
		||||
        tap((r) => {
 | 
			
		||||
          this.fields = r.results
 | 
			
		||||
        }),
 | 
			
		||||
        delay(100)
 | 
			
		||||
      )
 | 
			
		||||
      .subscribe(() => {
 | 
			
		||||
        this.reveal = true
 | 
			
		||||
        this.loading = false
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@
 | 
			
		||||
  </h4>
 | 
			
		||||
  <ul class="list-group">
 | 
			
		||||
    <li class="list-group-item">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
      <div class="row reveal">
 | 
			
		||||
        <div class="col" i18n>Name</div>
 | 
			
		||||
        <div class="col" i18n>Server</div>
 | 
			
		||||
        <div class="col d-none d-sm-block" i18n>Username</div>
 | 
			
		||||
@ -34,9 +34,16 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </li>
 | 
			
		||||
 | 
			
		||||
    @if (loadingAccounts) {
 | 
			
		||||
      <li class="list-group-item">
 | 
			
		||||
        <div class="spinner-border spinner-border-sm me-2" role="status"></div>
 | 
			
		||||
        <ng-container i18n>Loading...</ng-container>
 | 
			
		||||
      </li>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @for (account of mailAccounts; track account) {
 | 
			
		||||
      <li class="list-group-item">
 | 
			
		||||
        <div class="row">
 | 
			
		||||
        <div class="row" [class.reveal]="revealAccounts">
 | 
			
		||||
          <div class="col d-flex align-items-center">
 | 
			
		||||
            <button class="btn btn-link p-0 text-start" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">
 | 
			
		||||
              {{account.name}}@switch (account.account_type) {
 | 
			
		||||
@ -76,7 +83,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </li>
 | 
			
		||||
    }
 | 
			
		||||
    @if (mailAccounts.length === 0) {
 | 
			
		||||
    @if (!loadingAccounts && mailAccounts.length === 0) {
 | 
			
		||||
      <li class="list-group-item" i18n>No mail accounts defined.</li>
 | 
			
		||||
    }
 | 
			
		||||
  </ul>
 | 
			
		||||
@ -92,7 +99,7 @@
 | 
			
		||||
  </h4>
 | 
			
		||||
  <ul class="list-group">
 | 
			
		||||
    <li class="list-group-item">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
      <div class="row reveal">
 | 
			
		||||
        <div class="col" i18n>Name</div>
 | 
			
		||||
        <div class="col d-none d-sm-block" i18n>Sort Order</div>
 | 
			
		||||
        <div class="col" i18n>Account</div>
 | 
			
		||||
@ -101,9 +108,16 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </li>
 | 
			
		||||
 | 
			
		||||
    @if (loadingRules) {
 | 
			
		||||
      <li class="list-group-item">
 | 
			
		||||
        <div class="spinner-border spinner-border-sm me-2" role="status"></div>
 | 
			
		||||
        <ng-container i18n>Loading...</ng-container>
 | 
			
		||||
      </li>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @for (rule of mailRules; track rule) {
 | 
			
		||||
      <li class="list-group-item">
 | 
			
		||||
        <div class="row">
 | 
			
		||||
        <div class="row" [class.reveal]="revealRules">
 | 
			
		||||
          <div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div>
 | 
			
		||||
          <div class="col d-flex align-items-center d-none d-sm-flex">{{rule.order}}</div>
 | 
			
		||||
          <div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
 | 
			
		||||
@ -151,7 +165,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </li>
 | 
			
		||||
    }
 | 
			
		||||
    @if (mailRules.length === 0) {
 | 
			
		||||
    @if (!loadingRules && mailRules.length === 0) {
 | 
			
		||||
      <li class="list-group-item" i18n>No mail rules defined.</li>
 | 
			
		||||
    }
 | 
			
		||||
  </ul>
 | 
			
		||||
 | 
			
		||||
@ -2,3 +2,12 @@
 | 
			
		||||
.d-block.d-sm-none .dropdown-toggle::after {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-group-item .row {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  transition: opacity .2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-group-item .reveal {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -129,6 +129,8 @@ describe('MailComponent', () => {
 | 
			
		||||
    fixture = TestBed.createComponent(MailComponent)
 | 
			
		||||
    component = fixture.componentInstance
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    jest.useFakeTimers()
 | 
			
		||||
    jest.advanceTimersByTime(100)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  function completeSetup(excludeService = null) {
 | 
			
		||||
@ -386,6 +388,7 @@ describe('MailComponent', () => {
 | 
			
		||||
    component.oAuthAccountId = 3
 | 
			
		||||
    const editSpy = jest.spyOn(component, 'editMailAccount')
 | 
			
		||||
    component.ngOnInit()
 | 
			
		||||
    jest.advanceTimersByTime(200)
 | 
			
		||||
    expect(editSpy).toHaveBeenCalled()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy } from '@angular/core'
 | 
			
		||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { Subject, first, takeUntil } from 'rxjs'
 | 
			
		||||
import { Subject, delay, first, takeUntil, tap } from 'rxjs'
 | 
			
		||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
 | 
			
		||||
import { MailAccount, MailAccountType } from 'src/app/data/mail-account'
 | 
			
		||||
import { MailRule } from 'src/app/data/mail-rule'
 | 
			
		||||
@ -47,6 +47,11 @@ export class MailComponent
 | 
			
		||||
    return this.settingsService.get(SETTINGS_KEYS.OUTLOOK_OAUTH_URL)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public loadingRules: boolean = true
 | 
			
		||||
  public revealRules: boolean = false
 | 
			
		||||
  public loadingAccounts: boolean = true
 | 
			
		||||
  public revealAccounts: boolean = false
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public mailAccountService: MailAccountService,
 | 
			
		||||
    public mailRuleService: MailRuleService,
 | 
			
		||||
@ -62,9 +67,10 @@ export class MailComponent
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.mailAccountService
 | 
			
		||||
      .listAll(null, null, { full_perms: true })
 | 
			
		||||
      .pipe(first(), takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
      .subscribe({
 | 
			
		||||
        next: (r) => {
 | 
			
		||||
      .pipe(
 | 
			
		||||
        first(),
 | 
			
		||||
        takeUntil(this.unsubscribeNotifier),
 | 
			
		||||
        tap((r) => {
 | 
			
		||||
          this.mailAccounts = r.results
 | 
			
		||||
          if (this.oAuthAccountId) {
 | 
			
		||||
            this.editMailAccount(
 | 
			
		||||
@ -73,6 +79,13 @@ export class MailComponent
 | 
			
		||||
              )
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
        delay(100)
 | 
			
		||||
      )
 | 
			
		||||
      .subscribe({
 | 
			
		||||
        next: () => {
 | 
			
		||||
          this.loadingAccounts = false
 | 
			
		||||
          this.revealAccounts = true
 | 
			
		||||
        },
 | 
			
		||||
        error: (e) => {
 | 
			
		||||
          this.toastService.showError(
 | 
			
		||||
@ -84,10 +97,18 @@ export class MailComponent
 | 
			
		||||
 | 
			
		||||
    this.mailRuleService
 | 
			
		||||
      .listAll(null, null, { full_perms: true })
 | 
			
		||||
      .pipe(first(), takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
      .pipe(
 | 
			
		||||
        first(),
 | 
			
		||||
        takeUntil(this.unsubscribeNotifier),
 | 
			
		||||
        tap((r) => {
 | 
			
		||||
          this.mailRules = r.results
 | 
			
		||||
        }),
 | 
			
		||||
        delay(100)
 | 
			
		||||
      )
 | 
			
		||||
      .subscribe({
 | 
			
		||||
        next: (r) => {
 | 
			
		||||
          this.mailRules = r.results
 | 
			
		||||
          this.loadingRules = false
 | 
			
		||||
          this.revealRules = true
 | 
			
		||||
        },
 | 
			
		||||
        error: (e) => {
 | 
			
		||||
          this.toastService.showError($localize`Error retrieving mail rules`, e)
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@
 | 
			
		||||
        </tr>
 | 
			
		||||
      }
 | 
			
		||||
      @for (object of data; track object) {
 | 
			
		||||
        <tr (click)="toggleSelected(object); $event.stopPropagation();">
 | 
			
		||||
        <tr (click)="toggleSelected(object); $event.stopPropagation();" class="data-row" [class.reveal]="reveal">
 | 
			
		||||
          <td>
 | 
			
		||||
            <div class="form-check m-0 ms-2 me-n2">
 | 
			
		||||
              <input type="checkbox" class="form-check-input" id="{{typeName}}{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();">
 | 
			
		||||
 | 
			
		||||
@ -10,3 +10,12 @@ tbody tr:last-child td {
 | 
			
		||||
.form-check {
 | 
			
		||||
    min-height: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.data-row {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transition: opacity .2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.reveal {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -150,6 +150,7 @@ describe('ManagementListComponent', () => {
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    expect(component.nameFilter).toBeNull()
 | 
			
		||||
    expect(component.data).toEqual(tags)
 | 
			
		||||
    tick(100) // load
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  it('should support create, show notification on error / success', () => {
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,13 @@ import {
 | 
			
		||||
} from '@angular/core'
 | 
			
		||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { Subject } from 'rxjs'
 | 
			
		||||
import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'
 | 
			
		||||
import {
 | 
			
		||||
  debounceTime,
 | 
			
		||||
  delay,
 | 
			
		||||
  distinctUntilChanged,
 | 
			
		||||
  takeUntil,
 | 
			
		||||
  tap,
 | 
			
		||||
} from 'rxjs/operators'
 | 
			
		||||
import {
 | 
			
		||||
  MatchingModel,
 | 
			
		||||
  MATCHING_ALGORITHMS,
 | 
			
		||||
@ -89,6 +95,8 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
 | 
			
		||||
  public selectedObjects: Set<number> = new Set()
 | 
			
		||||
  public togggleAll: boolean = false
 | 
			
		||||
 | 
			
		||||
  public reveal: boolean = false
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.reloadData()
 | 
			
		||||
 | 
			
		||||
@ -132,7 +140,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
 | 
			
		||||
    this.reloadData()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  reloadData() {
 | 
			
		||||
  reloadData(extraParams: { [key: string]: any } = null) {
 | 
			
		||||
    this.isLoading = true
 | 
			
		||||
    this.clearSelection()
 | 
			
		||||
    this.service
 | 
			
		||||
@ -142,12 +150,19 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
 | 
			
		||||
        this.sortField,
 | 
			
		||||
        this.sortReverse,
 | 
			
		||||
        this._nameFilter,
 | 
			
		||||
        true
 | 
			
		||||
        true,
 | 
			
		||||
        extraParams
 | 
			
		||||
      )
 | 
			
		||||
      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
      .subscribe((c) => {
 | 
			
		||||
        this.data = c.results
 | 
			
		||||
        this.collectionSize = c.count
 | 
			
		||||
      .pipe(
 | 
			
		||||
        takeUntil(this.unsubscribeNotifier),
 | 
			
		||||
        tap((c) => {
 | 
			
		||||
          this.data = c.results
 | 
			
		||||
          this.collectionSize = c.count
 | 
			
		||||
        }),
 | 
			
		||||
        delay(100)
 | 
			
		||||
      )
 | 
			
		||||
      .subscribe(() => {
 | 
			
		||||
        this.reveal = true
 | 
			
		||||
        this.isLoading = false
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@
 | 
			
		||||
<ul class="list-group">
 | 
			
		||||
 | 
			
		||||
  <li class="list-group-item">
 | 
			
		||||
    <div class="row">
 | 
			
		||||
    <div class="row reveal">
 | 
			
		||||
      <div class="col" i18n>Name</div>
 | 
			
		||||
      <div class="col d-none d-sm-flex" i18n>Sort order</div>
 | 
			
		||||
      <div class="col" i18n>Status</div>
 | 
			
		||||
@ -22,9 +22,16 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </li>
 | 
			
		||||
 | 
			
		||||
  @if (loading) {
 | 
			
		||||
    <li class="list-group-item">
 | 
			
		||||
      <div class="spinner-border spinner-border-sm me-2" role="status"></div>
 | 
			
		||||
      <ng-container i18n>Loading...</ng-container>
 | 
			
		||||
    </li>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @for (workflow of workflows; track workflow.id) {
 | 
			
		||||
    <li class="list-group-item">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
      <div class="row" [class.reveal]="reveal">
 | 
			
		||||
        <div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editWorkflow(workflow)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Workflow)">{{workflow.name}}</button></div>
 | 
			
		||||
        <div class="col d-flex align-items-center d-none d-sm-flex"><code>{{workflow.order}}</code></div>
 | 
			
		||||
        <div class="col d-flex align-items-center">
 | 
			
		||||
@ -69,7 +76,7 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </li>
 | 
			
		||||
  }
 | 
			
		||||
  @if (workflows.length === 0) {
 | 
			
		||||
    <li class="list-group-item" i18n>No workflows defined.</li>
 | 
			
		||||
  @if (!loading && workflows.length === 0) {
 | 
			
		||||
    <li class="list-group-item" [class.reveal]="reveal" i18n>No workflows defined.</li>
 | 
			
		||||
  }
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
@ -2,3 +2,12 @@
 | 
			
		||||
.d-block.d-sm-none .dropdown-toggle::after {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-group-item .row {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  transition: opacity .2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-group-item .reveal {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -119,10 +119,11 @@ describe('WorkflowsComponent', () => {
 | 
			
		||||
    )
 | 
			
		||||
    modalService = TestBed.inject(NgbModal)
 | 
			
		||||
    toastService = TestBed.inject(ToastService)
 | 
			
		||||
 | 
			
		||||
    jest.useFakeTimers()
 | 
			
		||||
    fixture = TestBed.createComponent(WorkflowsComponent)
 | 
			
		||||
    component = fixture.componentInstance
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    jest.advanceTimersByTime(100)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support create, show notification on error / success', () => {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core'
 | 
			
		||||
import { WorkflowService } from 'src/app/services/rest/workflow.service'
 | 
			
		||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
 | 
			
		||||
import { Subject, takeUntil } from 'rxjs'
 | 
			
		||||
import { delay, Subject, takeUntil, tap } from 'rxjs'
 | 
			
		||||
import { Workflow } from 'src/app/data/workflow'
 | 
			
		||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { ToastService } from 'src/app/services/toast.service'
 | 
			
		||||
@ -26,6 +26,9 @@ export class WorkflowsComponent
 | 
			
		||||
 | 
			
		||||
  private unsubscribeNotifier: Subject<any> = new Subject()
 | 
			
		||||
 | 
			
		||||
  public loading: boolean = false
 | 
			
		||||
  public reveal: boolean = false
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private workflowService: WorkflowService,
 | 
			
		||||
    public permissionsService: PermissionsService,
 | 
			
		||||
@ -40,11 +43,17 @@ export class WorkflowsComponent
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  reload() {
 | 
			
		||||
    this.loading = true
 | 
			
		||||
    this.workflowService
 | 
			
		||||
      .listAll()
 | 
			
		||||
      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
      .subscribe((r) => {
 | 
			
		||||
        this.workflows = r.results
 | 
			
		||||
      .pipe(
 | 
			
		||||
        takeUntil(this.unsubscribeNotifier),
 | 
			
		||||
        tap((r) => (this.workflows = r.results)),
 | 
			
		||||
        delay(100)
 | 
			
		||||
      )
 | 
			
		||||
      .subscribe(() => {
 | 
			
		||||
        this.reveal = true
 | 
			
		||||
        this.loading = false
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -203,19 +203,23 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
 | 
			
		||||
 | 
			
		||||
  @supports (hanging-punctuation: first) and (font: -apple-system-body) and (-webkit-appearance: none) {
 | 
			
		||||
    // Safari does not like the filters on the image, see https://github.com/paperless-ngx/paperless-ngx/pull/8121
 | 
			
		||||
    .doc-img-container {
 | 
			
		||||
      background-color: #ffffff !important;
 | 
			
		||||
    }
 | 
			
		||||
    .document-card:not(.placeholder-glow),
 | 
			
		||||
    .document-card-large:not(.placeholder-glow) {
 | 
			
		||||
      .doc-img-container {
 | 
			
		||||
        background-color: #ffffff !important;
 | 
			
		||||
        width: 101%;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    .doc-img {
 | 
			
		||||
      filter: none !important;
 | 
			
		||||
      box-shadow: inset 0px 0px 0px 10px rgba(0,0,0,1);
 | 
			
		||||
    }
 | 
			
		||||
      .doc-img {
 | 
			
		||||
        filter: none !important;
 | 
			
		||||
        box-shadow: inset 0px 0px 0px 10px rgba(0,0,0,1);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    .doc-img.inverted {
 | 
			
		||||
      filter: none !important;
 | 
			
		||||
      mix-blend-mode: difference;
 | 
			
		||||
      opacity: 0.95;
 | 
			
		||||
      .doc-img.inverted {
 | 
			
		||||
        filter: none !important;
 | 
			
		||||
        mix-blend-mode: difference;
 | 
			
		||||
        opacity: 0.95;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user