Chore: Update Angular to v17 (#4980)

This commit is contained in:
shamoon
2023-12-19 15:02:05 -08:00
committed by GitHub
parent 1302e1d48a
commit daed79ee98
72 changed files with 7450 additions and 6557 deletions

View File

@@ -3,140 +3,144 @@
<button class="btn btn-sm btn-outline-secondary" (click)="list.selectNone()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#slash-circle" />
</svg>&nbsp;<ng-container i18n>Cancel</ng-container>
</button>
</div>
<div class="d-flex align-items-center gap-2" role="group" aria-label="Select">
<label class="me-2" i18n>Select:</label>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-check" />
</svg>&nbsp;<ng-container i18n>Page</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary" (click)="list.selectAll()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#check-all" />
</svg>&nbsp;<ng-container i18n>All</ng-container>
</svg>&nbsp;<ng-container i18n>Cancel</ng-container>
</button>
</div>
</div>
<div class="d-flex align-items-center gap-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<label class="me-2" i18n>Edit:</label>
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder
[items]="tags"
[disabled]="!userCanEditAll"
[editing]="true"
[manyToOne]="true"
[applyOnClose]="applyOnClose"
(opened)="openTagsDropdown()"
[(selectionModel)]="tagSelectionModel"
[documentCounts]="tagDocumentCounts"
(apply)="setTags($event)">
</pngx-filterable-dropdown>
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[items]="correspondents"
[disabled]="!userCanEditAll"
[editing]="true"
[applyOnClose]="applyOnClose"
(opened)="openCorrespondentDropdown()"
[(selectionModel)]="correspondentSelectionModel"
[documentCounts]="correspondentDocumentCounts"
(apply)="setCorrespondents($event)">
</pngx-filterable-dropdown>
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder
[items]="documentTypes"
[disabled]="!userCanEditAll"
[editing]="true"
[applyOnClose]="applyOnClose"
(opened)="openDocumentTypeDropdown()"
[(selectionModel)]="documentTypeSelectionModel"
[documentCounts]="documentTypeDocumentCounts"
(apply)="setDocumentTypes($event)">
</pngx-filterable-dropdown>
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[items]="storagePaths"
[disabled]="!userCanEditAll"
[editing]="true"
[applyOnClose]="applyOnClose"
(opened)="openStoragePathDropdown()"
[(selectionModel)]="storagePathsSelectionModel"
[documentCounts]="storagePathDocumentCounts"
(apply)="setStoragePaths($event)">
</pngx-filterable-dropdown>
</div>
<div class="d-flex align-items-center gap-2 ms-auto">
<div class="btn-toolbar">
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
</svg><div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Permissions</ng-container></div>
</button>
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
</svg>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Actions</ng-container></div>
</button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<button ngbDropdownItem (click)="redoOcrSelected()" [disabled]="!userCanEditAll" i18n>Redo OCR</button>
<div class="d-flex align-items-center gap-2" role="group" aria-label="Select">
<label class="me-2" i18n>Select:</label>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-check" />
</svg>&nbsp;<ng-container i18n>Page</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary" (click)="list.selectAll()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#check-all" />
</svg>&nbsp;<ng-container i18n>All</ng-container>
</button>
</div>
</div>
</div>
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
<svg *ngIf="!awaitingDownload" class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-down" />
</svg>
<div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Preparing download...</span>
<div class="d-flex align-items-center gap-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<label class="me-2" i18n>Edit:</label>
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder
[items]="tags"
[disabled]="!userCanEditAll"
[editing]="true"
[manyToOne]="true"
[applyOnClose]="applyOnClose"
(opened)="openTagsDropdown()"
[(selectionModel)]="tagSelectionModel"
[documentCounts]="tagDocumentCounts"
(apply)="setTags($event)">
</pngx-filterable-dropdown>
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[items]="correspondents"
[disabled]="!userCanEditAll"
[editing]="true"
[applyOnClose]="applyOnClose"
(opened)="openCorrespondentDropdown()"
[(selectionModel)]="correspondentSelectionModel"
[documentCounts]="correspondentDocumentCounts"
(apply)="setCorrespondents($event)">
</pngx-filterable-dropdown>
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder
[items]="documentTypes"
[disabled]="!userCanEditAll"
[editing]="true"
[applyOnClose]="applyOnClose"
(opened)="openDocumentTypeDropdown()"
[(selectionModel)]="documentTypeSelectionModel"
[documentCounts]="documentTypeDocumentCounts"
(apply)="setDocumentTypes($event)">
</pngx-filterable-dropdown>
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[items]="storagePaths"
[disabled]="!userCanEditAll"
[editing]="true"
[applyOnClose]="applyOnClose"
(opened)="openStoragePathDropdown()"
[(selectionModel)]="storagePathsSelectionModel"
[documentCounts]="storagePathDocumentCounts"
(apply)="setStoragePaths($event)">
</pngx-filterable-dropdown>
</div>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Download</ng-container></div>
</button>
<div ngbDropdown class="me-2 d-flex btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle-split rounded-end" ngbDropdownToggle></button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<form [formGroup]="downloadForm" class="px-3 py-1">
<p class="mb-1" i18n>Include:</p>
<div class="form-group ps-3 mb-2">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" />
<label class="form-check-label" for="downloadFileType_archive" i18n>
<div class="d-flex align-items-center gap-2 ms-auto">
<div class="btn-toolbar">
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
</svg><div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Permissions</ng-container></div>
</button>
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
</svg>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Actions</ng-container></div>
</button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<button ngbDropdownItem (click)="redoOcrSelected()" [disabled]="!userCanEditAll" i18n>Redo OCR</button>
</div>
</div>
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
@if (!awaitingDownload) {
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-down" />
</svg>
}
@if (awaitingDownload) {
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Preparing download...</span>
</div>
}
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Download</ng-container></div>
</button>
<div ngbDropdown class="me-2 d-flex btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle-split rounded-end" ngbDropdownToggle></button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<form [formGroup]="downloadForm" class="px-3 py-1">
<p class="mb-1" i18n>Include:</p>
<div class="form-group ps-3 mb-2">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" />
<label class="form-check-label" for="downloadFileType_archive" i18n>
Archived files
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" />
<label class="form-check-label" for="downloadFileType_originals" i18n>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" />
<label class="form-check-label" for="downloadFileType_originals" i18n>
Original files
</label>
</div>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" />
<label class="form-check-label" for="downloadUseFormatting" i18n>
</div>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" />
<label class="form-check-label" for="downloadUseFormatting" i18n>
Use formatted filename
</label>
</div>
</form>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</div>
</div>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</div>
</div>

View File

@@ -16,23 +16,35 @@
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title">
<ng-container *ngIf="document.correspondent">
<a *ngIf="clickCorrespondent.observers.length ; else nolink" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>
<ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>:
</ng-container>
@if (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}}
}
:
}
{{document.title | documentTitle}}
<pngx-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle *ngFor="let t of document.tags$ | async" class="ms-1" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="clickTag.observers.length"></pngx-tag>
@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>
}
</h5>
</div>
<p class="card-text">
<span *ngIf="document.__search_hit__ && document.__search_hit__.highlights" [innerHtml]="document.__search_hit__.highlights"></span>
<span *ngFor="let highlight of searchNoteHighlights" class="d-block">
<svg width="1em" height="1em" fill="currentColor" class="me-2">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
</svg>
<span [innerHtml]="highlight"></span>
</span>
<span *ngIf="!document.__search_hit__" class="result-content">{{contentTrimmed}}</span>
@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">
<svg width="1em" height="1em" fill="currentColor" class="me-2">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
</svg>
<span [innerHtml]="highlight"></span>
</span>
}
@if (!document.__search_hit__) {
<span class="result-content">{{contentTrimmed}}</span>
}
</p>
@@ -41,91 +53,105 @@
<a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()">
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
<use xlink:href="assets/bootstrap-icons.svg#diagram-3"/>
</svg>&nbsp;<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 }">
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
<use xlink:href="assets/bootstrap-icons.svg#pencil"/>
</svg>&nbsp;<span class="d-none d-md-inline" i18n>Edit</span>
</a>
<a class="btn btn-sm btn-outline-secondary" target="_blank" [href]="previewUrl"
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
<use xlink:href="assets/bootstrap-icons.svg#eye"/>
</svg>&nbsp;<span class="d-none d-md-inline" i18n>View</span>
</a>
<ng-template #previewContent>
<pngx-preview-popup [document]="document"></pngx-preview-popup>
</ng-template>
<a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
<use xlink:href="assets/bootstrap-icons.svg#download"/>
</svg>&nbsp;<span class="d-none d-md-inline" i18n>Download</span>
</a>
</div>
</svg>&nbsp;<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 }">
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
<use xlink:href="assets/bootstrap-icons.svg#pencil"/>
</svg>&nbsp;<span class="d-none d-md-inline" i18n>Edit</span>
</a>
<a class="btn btn-sm btn-outline-secondary" target="_blank" [href]="previewUrl"
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
<use xlink:href="assets/bootstrap-icons.svg#eye"/>
</svg>&nbsp;<span class="d-none d-md-inline" i18n>View</span>
</a>
<ng-template #previewContent>
<pngx-preview-popup [document]="document"></pngx-preview-popup>
</ng-template>
<a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
<use xlink:href="assets/bootstrap-icons.svg#download"/>
</svg>&nbsp;<span class="d-none d-md-inline" i18n>Download</span>
</a>
</div>
<div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
<button *ngIf="notesEnabled && document.notes.length" routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="View notes" i18n-title>
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
</svg>
<small i18n>{{document.notes.length}} Notes</small>
</button>
<button *ngIf="document.document_type" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by document type" i18n-title
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
</svg>
<small>{{(document.document_type$ | async)?.name}}</small>
</button>
<button *ngIf="document.storage_path" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by storage path" i18n-title
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#archive"/>
</svg>
<small>{{(document.storage_path$ | async)?.name}}</small>
</button>
<div *ngIf="document.archive_serial_number | isNumber" class="list-group-item me-2 bg-light text-dark p-1 border-0">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#upc-scan"/>
</svg>
<small>#{{document.archive_serial_number}}</small>
</div>
<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 class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
@if (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" title="View notes" i18n-title>
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
</svg>
<small i18n>{{document.notes.length}} Notes</small>
</button>
}
@if (document.document_type) {
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by document type" i18n-title
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
</svg>
<small>{{(document.document_type$ | async)?.name}}</small>
</button>
}
@if (document.storage_path) {
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by storage path" i18n-title
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#archive"/>
</svg>
<small>{{(document.storage_path$ | async)?.name}}</small>
</button>
}
@if (document.archive_serial_number | isNumber) {
<div class="list-group-item me-2 bg-light text-dark p-1 border-0">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#upc-scan"/>
</svg>
<small>#{{document.archive_serial_number}}</small>
</div>
}
<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="list-group-item bg-light text-dark p-1 border-0" [ngbTooltip]="dateTooltip">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#calendar-event"/>
</svg>
<small>{{document.created_date | customDate:'mediumDate'}}</small>
</div>
@if (document.owner && document.owner !== settingsService.currentUser.id) {
<div class="list-group-item bg-light text-dark p-1 border-0">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
</svg>
<small>{{document.owner | username}}</small>
</div>
}
@if (document.is_shared_by_requester) {
<div class="list-group-item bg-light text-dark p-1 border-0">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
</svg>
<small i18n>Shared</small>
</div>
}
@if (document.__search_hit__?.score) {
<div class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score">
<small class="text-muted" i18n>Score:</small>
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
</div>
}
</div>
</div>
</div>
</ng-template>
<div class="list-group-item bg-light text-dark p-1 border-0" [ngbTooltip]="dateTooltip">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#calendar-event"/>
</svg>
<small>{{document.created_date | customDate:'mediumDate'}}</small>
</div>
<div *ngIf="document.owner && document.owner !== settingsService.currentUser.id" class="list-group-item bg-light text-dark p-1 border-0">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
</svg>
<small>{{document.owner | username}}</small>
</div>
<div *ngIf="document.is_shared_by_requester" class="list-group-item bg-light text-dark p-1 border-0">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
</svg>
<small i18n>Shared</small>
</div>
<div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score">
<small class="text-muted" i18n>Score:</small>
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -11,45 +11,55 @@
</div>
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6">
<pngx-tag *ngFor="let t of getTagsLimited$() | async" [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag>
<div *ngIf="moreTags">
<span class="badge text-dark">+ {{moreTags}}</span>
</div>
@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>
}
@if (moreTags) {
<div>
<span class="badge text-dark">+ {{moreTags}}</span>
</div>
}
</div>
</div>
<a *ngIf="notesEnabled && document.notes.length" routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1">
<span class="badge rounded-pill bg-light border text-primary">
<svg class="metadata-icon ms-1 me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
</svg>
{{document.notes.length}}</span>
</a>
@if (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">
<svg class="metadata-icon ms-1 me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
</svg>
{{document.notes.length}}</span>
</a>
}
<div class="card-body bg-light p-2">
<p class="card-text">
<ng-container *ngIf="document.correspondent">
@if (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>:
</ng-container>
}
{{document.title | documentTitle}}
</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">
<button *ngIf="document.document_type" 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()">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
</svg>
<small>{{(document.document_type$ | async)?.name ?? privateName}}</small>
</button>
<button *ngIf="document.storage_path" 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()">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
</svg>
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
</button>
@if (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()">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
</svg>
<small>{{(document.document_type$ | async)?.name ?? privateName}}</small>
</button>
}
@if (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()">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
</svg>
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
</button>
}
<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">
@@ -65,24 +75,30 @@
<small>{{document.created_date | customDate:'mediumDate'}}</small>
</div>
</div>
<div *ngIf="document.archive_serial_number | isNumber" class="ps-0 p-1">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#upc-scan"/>
</svg>
<small>#{{document.archive_serial_number}}</small>
</div>
<div *ngIf="document.owner && document.owner !== settingsService.currentUser.id" class="ps-0 p-1">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
</svg>
<small>{{document.owner | username}}</small>
</div>
<div *ngIf="document.is_shared_by_requester" class="ps-0 p-1">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
</svg>
<small i18n>Shared</small>
</div>
@if (document.archive_serial_number | isNumber) {
<div class="ps-0 p-1">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#upc-scan"/>
</svg>
<small>#{{document.archive_serial_number}}</small>
</div>
}
@if (document.owner && document.owner !== settingsService.currentUser.id) {
<div class="ps-0 p-1">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
</svg>
<small>{{document.owner | username}}</small>
</div>
}
@if (document.is_shared_by_requester) {
<div class="ps-0 p-1">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
</svg>
<small i18n>Shared</small>
</div>
}
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group w-100">
@@ -92,8 +108,8 @@
</svg>
</a>
<a [href]="previewUrl" target="_blank" class="btn btn-sm btn-outline-secondary"
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>

View File

@@ -52,9 +52,11 @@
</label>
</div>
<div>
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSortField(f.field)"
[class.active]="list.sortField === f.field">{{f.name}}
</button>
@for (f of getSortFields(); track f) {
<button ngbDropdownItem (click)="setSortField(f.field)"
[class.active]="list.sortField === f.field">{{f.name}}
</button>
}
</div>
</div>
</div>
@@ -62,18 +64,26 @@
<div class="btn-group ms-2 flex-fill" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }" ngbDropdown role="group">
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle>
<ng-container i18n>Views</ng-container>
<div *ngIf="savedViewIsModified" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle">
<span class="visually-hidden">selected</span>
</div>
@if (savedViewIsModified) {
<div 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>
}
</button>
<div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu>
<ng-container *ngIf="!list.activeSavedViewId">
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view.id)">{{view.name}}</button>
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
</ng-container>
@if (!list.activeSavedViewId) {
@for (view of savedViewService.allViews; track view) {
<button ngbDropdownItem (click)="loadViewConfig(view.id)">{{view.name}}</button>
}
@if (savedViewService.allViews.length > 0) {
<div class="dropdown-divider"></div>
}
}
<div *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.SavedView }">
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button>
@if (list.activeSavedViewId) {
<button ngbDropdownItem (click)="saveViewConfig()" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button>
}
</div>
<button ngbDropdownItem (click)="saveViewConfigAs()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.SavedView }" i18n>Save as...</button>
</div>
@@ -90,165 +100,191 @@
<ng-template #pagination>
<div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<ng-container *ngIf="list.isReloading">
@if (list.isReloading) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</ng-container>
<span i18n *ngIf="list.selected.size > 0">{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span>
<ng-container *ngIf="!list.isReloading">
<span i18n *ngIf="list.selected.size === 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>&nbsp;<span i18n *ngIf="isFiltered">(filtered)</span>
</ng-container>
<button *ngIf="!list.isReloading && isFiltered" class="btn btn-link py-0" (click)="resetFilters()">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg><small i18n>Reset filters</small>
</button>
}
@if (list.selected.size > 0) {
<span i18n>{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span>
}
@if (!list.isReloading) {
@if (list.selected.size === 0) {
<span i18n>{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>
}&nbsp;@if (isFiltered) {
<span i18n>(filtered)</span>
}
}
@if (!list.isReloading && isFiltered) {
<button class="btn btn-link py-0" (click)="resetFilters()">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg><small i18n>Reset filters</small>
</button>
}
</div>
@if (list.collectionSize) {
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination>
}
</div>
<ngb-pagination *ngIf="list.collectionSize" [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination>
</div>
</ng-template>
</ng-template>
<div tourAnchor="tour.documents">
<ng-container *ngTemplateOutlet="pagination"></ng-container>
</div>
<ng-container *ngIf="list.error ; else documentListNoError">
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
</ng-container>
<ng-template #documentListNoError>
<div *ngIf="displayMode === 'largeCards'">
<pngx-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickStoragePath)="clickStoragePath($event)" (clickMoreLike)="clickMoreLike(d.id)">
</pngx-document-card-large>
</div>
<table class="table table-sm align-middle border shadow-sm" *ngIf="displayMode === 'details'">
<thead>
<th></th>
<th class="d-none d-lg-table-cell"
pngxSortable="archive_serial_number"
title="Sort by ASN" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>ASN</th>
<th class="d-none d-md-table-cell"
pngxSortable="correspondent__name"
title="Sort by correspondent" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Correspondent</th>
<th
pngxSortable="title"
title="Sort by title" i18n-title
class="w-40"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Title</th>
<th class="d-none d-xl-table-cell"
pngxSortable="owner"
title="Sort by owner" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Owner</th>
<th *ngIf="notesEnabled" class="d-none d-xl-table-cell"
pngxSortable="num_notes"
title="Sort by notes" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Notes</th>
<th class="d-none d-xl-table-cell"
pngxSortable="document_type__name"
title="Sort by document type" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Document type</th>
<th class="d-none d-xl-table-cell"
pngxSortable="storage_path__name"
title="Sort by storage path" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Storage path</th>
<th
pngxSortable="created"
title="Sort by created date" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Created</th>
<th class="d-none d-xl-table-cell"
pngxSortable="added"
title="Sort by added date" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Added</th>
</thead>
<tbody>
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" (click)="toggleSelected(d, $event); $event.stopPropagation();" (dblclick)="openDocumentDetail(d)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event); $event.stopPropagation();">
<label class="form-check-label" for="docCheck{{d.id}}"></label>
</div>
</td>
<td class="d-none d-lg-table-cell">
{{d.archive_serial_number}}
</td>
<td class="d-none d-md-table-cell">
<ng-container *ngIf="d.correspondent">
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
</ng-container>
</td>
<td>
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
<pngx-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag>
</td>
<td>
{{d.owner | username}}
</td>
<td *ngIf="notesEnabled" class="d-none d-xl-table-cell">
<a *ngIf="d.notes.length" routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
<span class="badge rounded-pill bg-light border text-primary">
<svg class="metadata-icon ms-1 me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
</svg>
{{d.notes.length}}</span>
</a>
</td>
<td class="d-none d-xl-table-cell">
<ng-container *ngIf="d.document_type">
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
</ng-container>
</td>
<td class="d-none d-xl-table-cell">
<ng-container *ngIf="d.storage_path">
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a>
</ng-container>
</td>
<td>
{{d.created_date | customDate}}
</td>
<td class="d-none d-xl-table-cell">
{{d.added | customDate}}
</td>
</tr>
</tbody>
</table>
<div class="row row-cols-paperless-cards" *ngIf="displayMode === 'smallCards'">
<pngx-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></pngx-document-card-small>
</div>
<div *ngIf="list.documents?.length > 15" class="mt-3">
<div tourAnchor="tour.documents">
<ng-container *ngTemplateOutlet="pagination"></ng-container>
</div>
</ng-template>
@if (list.error ) {
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
} @else {
@if (displayMode === 'largeCards') {
<div>
@for (d of list.documents; track trackByDocumentId($index, d)) {
<pngx-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickStoragePath)="clickStoragePath($event)" (clickMoreLike)="clickMoreLike(d.id)">
</pngx-document-card-large>
}
</div>
}
@if (displayMode === 'details') {
<table class="table table-sm align-middle border shadow-sm">
<thead>
<th></th>
<th class="d-none d-lg-table-cell"
pngxSortable="archive_serial_number"
title="Sort by ASN" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>ASN</th>
<th class="d-none d-md-table-cell"
pngxSortable="correspondent__name"
title="Sort by correspondent" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Correspondent</th>
<th
pngxSortable="title"
title="Sort by title" i18n-title
class="w-40"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Title</th>
<th class="d-none d-xl-table-cell"
pngxSortable="owner"
title="Sort by owner" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Owner</th>
@if (notesEnabled) {
<th class="d-none d-xl-table-cell"
pngxSortable="num_notes"
title="Sort by notes" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Notes</th>
}
<th class="d-none d-xl-table-cell"
pngxSortable="document_type__name"
title="Sort by document type" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Document type</th>
<th class="d-none d-xl-table-cell"
pngxSortable="storage_path__name"
title="Sort by storage path" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Storage path</th>
<th
pngxSortable="created"
title="Sort by created date" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Created</th>
<th class="d-none d-xl-table-cell"
pngxSortable="added"
title="Sort by added date" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Added</th>
</thead>
<tbody>
@for (d of list.documents; track trackByDocumentId($index, d)) {
<tr (click)="toggleSelected(d, $event); $event.stopPropagation();" (dblclick)="openDocumentDetail(d)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event); $event.stopPropagation();">
<label class="form-check-label" for="docCheck{{d.id}}"></label>
</div>
</td>
<td class="d-none d-lg-table-cell">
{{d.archive_serial_number}}
</td>
<td class="d-none d-md-table-cell">
@if (d.correspondent) {
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
}
</td>
<td>
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
@for (t of d.tags$ | async; track t) {
<pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag>
}
</td>
<td>
{{d.owner | username}}
</td>
@if (notesEnabled) {
<td class="d-none d-xl-table-cell">
@if (d.notes.length) {
<a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
<span class="badge rounded-pill bg-light border text-primary">
<svg class="metadata-icon ms-1 me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
</svg>
{{d.notes.length}}</span>
</a>
}
</td>
}
<td class="d-none d-xl-table-cell">
@if (d.document_type) {
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
}
</td>
<td class="d-none d-xl-table-cell">
@if (d.storage_path) {
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a>
}
</td>
<td>
{{d.created_date | customDate}}
</td>
<td class="d-none d-xl-table-cell">
{{d.added | customDate}}
</td>
</tr>
}
</tbody>
</table>
}
@if (displayMode === 'smallCards') {
<div class="row row-cols-paperless-cards">
@for (d of list.documents; track trackByDocumentId($index, d)) {
<pngx-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></pngx-document-card-small>
}
</div>
}
@if (list.documents?.length > 15) {
<div class="mt-3">
<ng-container *ngTemplateOutlet="pagination"></ng-container>
</div>
}
}

View File

@@ -5,17 +5,25 @@
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
<button *ngFor="let t of textFilterTargets" ngbDropdownItem [class.active]="textFilterTarget === t.id" (click)="changeTextFilterTarget(t.id)">{{t.name}}</button>
@for (t of textFilterTargets; track t) {
<button ngbDropdownItem [class.active]="textFilterTarget === t.id" (click)="changeTextFilterTarget(t.id)">{{t.name}}</button>
}
</div>
</div>
<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>
</select>
<button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()">
<svg fill="currentColor" class="buttonicon-sm me-1">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
</button>
@if (textFilterTarget === 'asn') {
<select class="form-select flex-grow-0 w-auto" [(ngModel)]="textFilterModifier" (change)="textFilterModifierChange()">
@for (m of textFilterModifiers; track m) {
<option ngbDropdownItem [value]="m.id">{{m.label}}</option>
}
</select>
}
@if (_textFilter) {
<button class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()">
<svg fill="currentColor" class="buttonicon-sm me-1">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</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>
@@ -31,7 +39,7 @@
(selectionModelChange)="updateRules()"
(opened)="onTagsDropdownOpen()"
[documentCounts]="tagDocumentCounts"
[allowSelectNone]="true"></pngx-filterable-dropdown>
[allowSelectNone]="true"></pngx-filterable-dropdown>
<pngx-filterable-dropdown class="flex-fill" title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[items]="correspondents"
@@ -39,7 +47,7 @@
(selectionModelChange)="updateRules()"
(opened)="onCorrespondentDropdownOpen()"
[documentCounts]="correspondentDocumentCounts"
[allowSelectNone]="true"></pngx-filterable-dropdown>
[allowSelectNone]="true"></pngx-filterable-dropdown>
<pngx-filterable-dropdown class="flex-fill" title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder
[items]="documentTypes"
@@ -47,7 +55,7 @@
(selectionModelChange)="updateRules()"
(opened)="onDocumentTypeDropdownOpen()"
[documentCounts]="documentTypeDocumentCounts"
[allowSelectNone]="true"></pngx-filterable-dropdown>
[allowSelectNone]="true"></pngx-filterable-dropdown>
<pngx-filterable-dropdown class="flex-fill" title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[items]="storagePaths"
@@ -55,7 +63,7 @@
(selectionModelChange)="updateRules()"
(opened)="onStoragePathDropdownOpen()"
[documentCounts]="storagePathDocumentCounts"
[allowSelectNone]="true"></pngx-filterable-dropdown>
[allowSelectNone]="true"></pngx-filterable-dropdown>
</div>
<div class="d-flex flex-wrap gap-2">
<pngx-date-dropdown
@@ -63,27 +71,27 @@
(datesSet)="updateRules()"
[(dateBefore)]="dateCreatedBefore"
[(dateAfter)]="dateCreatedAfter"
[(relativeDate)]="dateCreatedRelativeDate"></pngx-date-dropdown>
[(relativeDate)]="dateCreatedRelativeDate"></pngx-date-dropdown>
<pngx-date-dropdown
title="Added" i18n-title
(datesSet)="updateRules()"
[(dateBefore)]="dateAddedBefore"
[(dateAfter)]="dateAddedAfter"
[(relativeDate)]="dateAddedRelativeDate"></pngx-date-dropdown>
[(relativeDate)]="dateAddedRelativeDate"></pngx-date-dropdown>
</div>
<div class="d-flex flex-wrap">
<pngx-permissions-filter-dropdown
title="Permissions" i18n-title
(ownerFilterSet)="updateRules()"
[(selectionModel)]="permissionsSelectionModel"></pngx-permissions-filter-dropdown>
[(selectionModel)]="permissionsSelectionModel"></pngx-permissions-filter-dropdown>
</div>
<div class="d-flex flex-wrap d-none d-sm-inline-block">
<button class="btn btn-outline-secondary btn-sm" [disabled]="!rulesModified" (click)="resetSelected()">
<svg class="toolbaricon ms-n1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"></use>
</svg><ng-container i18n>Reset filters</ng-container>
</button>
</svg><ng-container i18n>Reset filters</ng-container>
</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1726,4 +1726,13 @@ describe('FilterEditorComponent', () => {
)
expect(component.textFilter).toEqual('')
})
it('should adjust text filter targets if more like search', () => {
const TEXT_FILTER_TARGET_FULLTEXT_MORELIKE = 'fulltext-morelike' // private const
component.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
expect(component.textFilterTargets).toContainEqual({
id: TEXT_FILTER_TARGET_FULLTEXT_MORELIKE,
name: $localize`More like`,
})
})
})

View File

@@ -105,6 +105,51 @@ const RELATIVE_DATE_QUERYSTRINGS = [
},
]
const DEFAULT_TEXT_FILTER_TARGET_OPTIONS = [
{ id: TEXT_FILTER_TARGET_TITLE, name: $localize`Title` },
{
id: TEXT_FILTER_TARGET_TITLE_CONTENT,
name: $localize`Title & content`,
},
{ id: TEXT_FILTER_TARGET_ASN, name: $localize`ASN` },
{
id: TEXT_FILTER_TARGET_CUSTOM_FIELDS,
name: $localize`Custom fields`,
},
{
id: TEXT_FILTER_TARGET_FULLTEXT_QUERY,
name: $localize`Advanced search`,
},
]
const TEXT_FILTER_TARGET_MORELIKE_OPTION = {
id: TEXT_FILTER_TARGET_FULLTEXT_MORELIKE,
name: $localize`More like`,
}
const DEFAULT_TEXT_FILTER_MODIFIER_OPTIONS = [
{
id: TEXT_FILTER_MODIFIER_EQUALS,
label: $localize`equals`,
},
{
id: TEXT_FILTER_MODIFIER_NULL,
label: $localize`is empty`,
},
{
id: TEXT_FILTER_MODIFIER_NOTNULL,
label: $localize`is not empty`,
},
{
id: TEXT_FILTER_MODIFIER_GT,
label: $localize`greater than`,
},
{
id: TEXT_FILTER_MODIFIER_LT,
label: $localize`less than`,
},
]
@Component({
selector: 'pngx-filter-editor',
templateUrl: './filter-editor.component.html',
@@ -200,29 +245,12 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
_moreLikeDoc: PaperlessDocument
get textFilterTargets() {
let targets = [
{ id: TEXT_FILTER_TARGET_TITLE, name: $localize`Title` },
{
id: TEXT_FILTER_TARGET_TITLE_CONTENT,
name: $localize`Title & content`,
},
{ id: TEXT_FILTER_TARGET_ASN, name: $localize`ASN` },
{
id: TEXT_FILTER_TARGET_CUSTOM_FIELDS,
name: $localize`Custom fields`,
},
{
id: TEXT_FILTER_TARGET_FULLTEXT_QUERY,
name: $localize`Advanced search`,
},
]
if (this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE) {
targets.push({
id: TEXT_FILTER_TARGET_FULLTEXT_MORELIKE,
name: $localize`More like`,
})
return DEFAULT_TEXT_FILTER_TARGET_OPTIONS.concat([
TEXT_FILTER_TARGET_MORELIKE_OPTION,
])
}
return targets
return DEFAULT_TEXT_FILTER_TARGET_OPTIONS
}
textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT
@@ -235,28 +263,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
public textFilterModifier: string
get textFilterModifiers() {
return [
{
id: TEXT_FILTER_MODIFIER_EQUALS,
label: $localize`equals`,
},
{
id: TEXT_FILTER_MODIFIER_NULL,
label: $localize`is empty`,
},
{
id: TEXT_FILTER_MODIFIER_NOTNULL,
label: $localize`is not empty`,
},
{
id: TEXT_FILTER_MODIFIER_GT,
label: $localize`greater than`,
},
{
id: TEXT_FILTER_MODIFIER_LT,
label: $localize`less than`,
},
]
return DEFAULT_TEXT_FILTER_MODIFIER_OPTIONS
}
get textFilterModifierIsNull(): boolean {

View File

@@ -8,11 +8,13 @@
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></pngx-input-check>
<pngx-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></pngx-input-check>
<div *ngIf="error?.filter_rules" class="alert alert-danger" role="alert">
<h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6>
<ng-container i18n>The error returned was</ng-container>:<br/>
{{ error.filter_rules }}
</div>
@if (error?.filter_rules) {
<div class="alert alert-danger" role="alert">
<h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6>
<ng-container i18n>The error returned was</ng-container>:<br/>
{{ error.filter_rules }}
</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="!buttonsEnabled">Cancel</button>