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

@@ -8,25 +8,31 @@
cdkDropList
[cdkDropListDisabled]="settingsService.globalDropzoneActive"
(cdkDropListDropped)="onDrop($event)"
>
<div *ngIf="savedViewService.loading" class="col">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</div>
>
@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>
}
<div *ngIf="settingsService.offerTour()" class="col">
<pngx-welcome-widget (dismiss)="completeTour()"></pngx-welcome-widget>
</div>
@if (settingsService.offerTour()) {
<div class="col">
<pngx-welcome-widget (dismiss)="completeTour()"></pngx-welcome-widget>
</div>
}
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<div *ngFor="let v of dashboardViews" class="col">
<pngx-saved-view-widget
[savedView]="v"
(cdkDragStarted)="onDragStart($event)"
(cdkDragEnded)="onDragEnd($event)"
>
</pngx-saved-view-widget>
</div>
@for (v of dashboardViews; track v) {
<div class="col">
<pngx-saved-view-widget
[savedView]="v"
(cdkDragStarted)="onDragStart($event)"
(cdkDragEnded)="onDragEnd($event)"
>
</pngx-saved-view-widget>
</div>
}
</ng-container>
</div>
</div>

View File

@@ -3,54 +3,63 @@
[title]="savedView.name"
[loading]="loading"
[draggable]="savedView"
>
>
<a *ngIf="documents.length" class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
@if (documents.length) {
<a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
}
<table *ngIf="documents.length; else empty" content class="table table-hover mb-0 align-middle">
<thead>
<tr>
<th scope="col" i18n>Created</th>
<th scope="col" i18n>Title</th>
<th scope="col" class="d-none d-md-table-cell" i18n>Tags</th>
<th scope="col" class="d-none d-md-table-cell" i18n>Correspondent</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let doc of documents" (mouseleave)="maybeClosePopover()">
<td class="py-2 py-md-3"><a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.created_date | customDate}}</a></td>
<td class="py-2 py-md-3">
<a routerLink="/documents/{{doc.id}}" title="Edit" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
</td>
<td class="py-2 py-md-3 d-none d-md-table-cell">
<pngx-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag>
</td>
<td class="position-relative py-2 py-md-3 d-none d-md-table-cell">
<a *ngIf="doc.correspondent !== null" class="btn-link text-dark text-decoration-none py-2 py-md-3" routerLink="/documents" [queryParams]="getCorrespondentQueryParams(doc.correspondent)">{{(doc.correspondent$ | async)?.name}}</a>
<div class="btn-group position-absolute top-50 end-0 translate-middle-y">
<a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle"
[ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle"
autoClose="true" popoverClass="shadow popover-preview" container="body" (mouseenter)="mouseEnterPreviewButton(doc)" (mouseleave)="mouseLeavePreviewButton()" #popover="ngbPopover">
<svg class="buttonicon-xs" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#eye"/>
</svg>
</a>
<ng-template #previewContent>
<pngx-preview-popup [document]="doc" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()"></pngx-preview-popup>
</ng-template>
<a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
<svg class="buttonicon-xs" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#download"/>
</svg>
</a>
</div>
</td>
</tr>
</tbody>
</table>
<ng-template #empty>
@if (documents.length) {
<table content class="table table-hover mb-0 align-middle">
<thead>
<tr>
<th scope="col" i18n>Created</th>
<th scope="col" i18n>Title</th>
<th scope="col" class="d-none d-md-table-cell" i18n>Tags</th>
<th scope="col" class="d-none d-md-table-cell" i18n>Correspondent</th>
</tr>
</thead>
<tbody>
@for (doc of documents; track doc) {
<tr (mouseleave)="maybeClosePopover()">
<td class="py-2 py-md-3"><a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.created_date | customDate}}</a></td>
<td class="py-2 py-md-3">
<a routerLink="/documents/{{doc.id}}" title="Edit" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
</td>
<td class="py-2 py-md-3 d-none d-md-table-cell">
@for (t of doc.tags$ | async; track t) {
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag>
}
</td>
<td class="position-relative py-2 py-md-3 d-none d-md-table-cell">
@if (doc.correspondent !== null) {
<a class="btn-link text-dark text-decoration-none py-2 py-md-3" routerLink="/documents" [queryParams]="getCorrespondentQueryParams(doc.correspondent)">{{(doc.correspondent$ | async)?.name}}</a>
}
<div class="btn-group position-absolute top-50 end-0 translate-middle-y">
<a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle"
[ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle"
autoClose="true" popoverClass="shadow popover-preview" container="body" (mouseenter)="mouseEnterPreviewButton(doc)" (mouseleave)="mouseLeavePreviewButton()" #popover="ngbPopover">
<svg class="buttonicon-xs" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#eye"/>
</svg>
</a>
<ng-template #previewContent>
<pngx-preview-popup [document]="doc" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()"></pngx-preview-popup>
</ng-template>
<a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
<svg class="buttonicon-xs" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#download"/>
</svg>
</a>
</div>
</td>
</tr>
}
</tbody>
</table>
} @else {
<p i18n class="text-center text-muted mb-0 fst-italic">No documents</p>
</ng-template>
}
</pngx-widget-frame>

View File

@@ -1,10 +1,12 @@
<pngx-widget-frame title="Statistics" [loading]="loading" i18n-title>
<ng-container content>
<div class="list-group border-light">
<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)" *ngIf="statistics?.documents_inbox !== null" (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>
@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>
@@ -13,60 +15,74 @@
<ng-container i18n>Total characters</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.character_count | number}}</span>
</div>
<div *ngIf="statistics?.document_file_type_counts?.length > 1" class="list-group-item filetypes">
<div class="d-flex justify-content-between align-items-center my-2">
<div class="progress flex-grow-1">
<div *ngFor="let filetype of statistics?.document_file_type_counts; let i = index; let last = last"
class="progress-bar bg-primary"
role="progressbar"
[ngbPopover]="getFileTypeName(filetype)"
i18n-ngbPopover
triggers="mouseenter:mouseleave"
[attr.aria-label]="getFileTypeName(filetype)"
[class.me-1px]="!last"
[style.width]="getFileTypePercent(filetype) + '%'"
[style.opacity]="getItemOpacity(i)"
[attr.aria-valuenow]="getFileTypePercent(filetype)"
aria-valuemin="0"
aria-valuemax="100">
@if (statistics?.document_file_type_counts?.length > 1) {
<div class="list-group-item filetypes">
<div class="d-flex justify-content-between align-items-center my-2">
<div class="progress flex-grow-1">
@for (filetype of statistics?.document_file_type_counts; track filetype; let i = $index; let last = $last) {
<div
class="progress-bar bg-primary"
role="progressbar"
[ngbPopover]="getFileTypeName(filetype)"
i18n-ngbPopover
triggers="mouseenter:mouseleave"
[attr.aria-label]="getFileTypeName(filetype)"
[class.me-1px]="!last"
[style.width]="getFileTypePercent(filetype) + '%'"
[style.opacity]="getItemOpacity(i)"
[attr.aria-valuenow]="getFileTypePercent(filetype)"
aria-valuemin="0"
aria-valuemax="100">
</div>
}
</div>
</div>
</div>
<div class="d-flex flex-wrap align-items-start">
<div class="d-flex" *ngFor="let filetype of statistics?.document_file_type_counts; let i = index">
<div class="text-nowrap me-2">
<span class="badge rounded-pill bg-primary d-inline-block p-0 me-1" [style.opacity]="getItemOpacity(i)"></span>
<small class="text-nowrap"><span class="fw-bold">{{ getFileTypeExtension(filetype) }}</span>&nbsp;<span class="text-muted">({{getFileTypePercent(filetype) | number: '1.0-1'}}%)</span></small>
</div>
<div class="d-flex flex-wrap align-items-start">
@for (filetype of statistics?.document_file_type_counts; track filetype; let i = $index) {
<div class="d-flex">
<div class="text-nowrap me-2">
<span class="badge rounded-pill bg-primary d-inline-block p-0 me-1" [style.opacity]="getItemOpacity(i)"></span>
<small class="text-nowrap"><span class="fw-bold">{{ getFileTypeExtension(filetype) }}</span>&nbsp;<span class="text-muted">({{getFileTypePercent(filetype) | number: '1.0-1'}}%)</span></small>
</div>
</div>
}
</div>
</div>
</div>
}
</div>
<div class="list-group border-light mt-3">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
<a *ngIf="statistics?.tag_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/tags/">
<ng-container i18n>Tags</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.tag_count | number}}</span>
</a>
@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>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.tag_count | number}}</span>
</a>
}
</ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
<a *ngIf="statistics?.correspondent_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/correspondents/">
<ng-container i18n>Correspondents</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.correspondent_count | number}}</span>
</a>
@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>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.correspondent_count | number}}</span>
</a>
}
</ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
<a *ngIf="statistics?.document_type_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documenttypes/">
<ng-container i18n>Document Types</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.document_type_count | number}}</span>
</a>
@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>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.document_type_count | number}}</span>
</a>
}
</ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<a *ngIf="statistics?.storage_path_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/storagepaths/">
<ng-container i18n>Storage Paths</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.storage_path_count | number}}</span>
</a>
@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>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.storage_path_count | number}}</span>
</a>
}
</ng-container>
</div>
</ng-container>

View File

@@ -8,32 +8,44 @@
<div class="fixed-bottom p-2 p-md-4" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'offset-md-3 offset-lg-2'">
<div class="row d-flex justify-content-end">
<div class="col col-lg-4 col-xl-3 d-flex px-4 justify-content-between align-items-center">
<p class="m-0 small text-muted" *ngIf="getStatus().length > 0">{{getStatusSummary()}}</p>
<a *ngIf="getStatusCompleted().length > 0" class="btn-link" (click)="dismissCompleted()" [routerLink]="[]" >
<span class="me-1" i18n="This button dismisses all status messages about processed documents on the dashboard (failed and successful)">Dismiss completed</span>
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-check2-all" viewBox="0 0 16 16">
<path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7zm-4.208 7l-.896-.897.707-.707.543.543 6.646-6.647a.5.5 0 0 1 .708.708l-7 7a.5.5 0 0 1-.708 0z"/>
<path d="M5.354 7.146l.896.897-.707.707-.897-.896a.5.5 0 1 1 .708-.708z"/>
</svg>
</a>
@if (getStatus().length > 0) {
<p class="m-0 small text-muted">{{getStatusSummary()}}</p>
}
@if (getStatusCompleted().length > 0) {
<a class="btn-link" (click)="dismissCompleted()" [routerLink]="[]" >
<span class="me-1" i18n="This button dismisses all status messages about processed documents on the dashboard (failed and successful)">Dismiss completed</span>
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-check2-all" viewBox="0 0 16 16">
<path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7zm-4.208 7l-.896-.897.707-.707.543.543 6.646-6.647a.5.5 0 0 1 .708.708l-7 7a.5.5 0 0 1-.708 0z"/>
<path d="M5.354 7.146l.896.897-.707.707-.897-.896a.5.5 0 1 1 .708-.708z"/>
</svg>
</a>
}
</div>
</div>
<div *ngFor="let status of getStatus()">
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
</div>
</div>
<div *ngIf="getStatusHidden().length" class="alerts-hidden">
<p *ngIf="!alertsExpanded" class="mt-3 mb-0 text-center">
<span i18n="This is shown as a summary line when there are more than 5 document in the processing pipeline.">{getStatusHidden().length, plural, =1 {One more document} other {{{getStatusHidden().length}} more documents}}</span>
&nbsp;&bull;&nbsp;
<a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a>
</p>
<div #hiddenAlerts="ngbCollapse" [(ngbCollapse)]="!alertsExpanded">
<div *ngFor="let status of getStatusHidden()">
@for (status of getStatus(); track status) {
<div>
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
</div>
</div>
}
</div>
@if (getStatusHidden().length) {
<div class="alerts-hidden">
@if (!alertsExpanded) {
<p class="mt-3 mb-0 text-center">
<span i18n="This is shown as a summary line when there are more than 5 document in the processing pipeline.">{getStatusHidden().length, plural, =1 {One more document} other {{{getStatusHidden().length}} more documents}}</span>
&nbsp;&bull;&nbsp;
<a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a>
</p>
}
<div #hiddenAlerts="ngbCollapse" [(ngbCollapse)]="!alertsExpanded">
@for (status of getStatusHidden(); track status) {
<div>
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
</div>
}
</div>
</div>
}
</div>
</pngx-widget-frame>
@@ -42,17 +54,23 @@
<div class="col col-lg-4 col-xl-3">
<ngb-alert type="secondary" class="mt-2 mb-0" [dismissible]="isFinished(status)" (closed)="dismiss(status)">
<h6 class="alert-heading">{{status.filename}}</h6>
<p class="mb-0 pb-1" *ngIf="!isFinished(status) || (isFinished(status) && !status.documentId)">{{status.message}}</p>
@if (!isFinished(status) || (isFinished(status) && !status.documentId)) {
<p class="mb-0 pb-1">{{status.message}}</p>
}
<ngb-progressbar [value]="status.getProgress()" [max]="1" [type]="getStatusColor(status)"></ngb-progressbar>
<div *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<div *ngIf="isFinished(status)">
<button *ngIf="status.documentId" class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
<small i18n>Open document</small>
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/>
</svg>
</button>
</div>
@if (isFinished(status)) {
<div>
@if (status.documentId) {
<button class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
<small i18n>Open document</small>
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/>
</svg>
</button>
}
</div>
}
</div>
</ngb-alert>
</div>

View File

@@ -27,6 +27,24 @@ import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
import { UploadFileWidgetComponent } from './upload-file-widget.component'
import { DragDropModule } from '@angular/cdk/drag-drop'
const FAILED_STATUSES = [new FileStatus()]
const WORKING_STATUSES = [new FileStatus(), new FileStatus()]
const STARTED_STATUSES = [new FileStatus(), new FileStatus(), new FileStatus()]
const SUCCESS_STATUSES = [
new FileStatus(),
new FileStatus(),
new FileStatus(),
new FileStatus(),
]
const DEFAULT_STATUSES = [
new FileStatus(),
new FileStatus(),
new FileStatus(),
new FileStatus(),
new FileStatus(),
new FileStatus(),
]
describe('UploadFileWidgetComponent', () => {
let component: UploadFileWidgetComponent
let fixture: ComponentFixture<UploadFileWidgetComponent>
@@ -150,41 +168,22 @@ function mockConsumerStatuses(consumerStatusService) {
.mockImplementation((phase) => {
switch (phase) {
case FileStatusPhase.FAILED:
return [new FileStatus()]
return FAILED_STATUSES
case FileStatusPhase.WORKING:
return [new FileStatus(), new FileStatus()]
return WORKING_STATUSES
case FileStatusPhase.STARTED:
return [new FileStatus(), new FileStatus(), new FileStatus()]
return STARTED_STATUSES
case FileStatusPhase.SUCCESS:
return [
new FileStatus(),
new FileStatus(),
new FileStatus(),
new FileStatus(),
]
return SUCCESS_STATUSES
case FileStatusPhase.UPLOADING:
return [partialUpload1, partialUpload2]
default:
return [
new FileStatus(),
new FileStatus(),
new FileStatus(),
new FileStatus(),
new FileStatus(),
new FileStatus(),
]
return DEFAULT_STATUSES
}
})
jest
.spyOn(consumerStatusService, 'getConsumerStatusNotCompleted')
.mockImplementation(() => {
return [
new FileStatus(),
new FileStatus(),
new FileStatus(),
new FileStatus(),
new FileStatus(),
new FileStatus(),
]
return DEFAULT_STATUSES
})
}

View File

@@ -2,17 +2,19 @@
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex">
<div *ngIf="draggable" class="ms-n2 me-1" cdkDragHandle>
<svg class="sidebaricon text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/>
</svg>
</div>
@if (draggable) {
<div class="ms-n2 me-1" cdkDragHandle>
<svg class="sidebaricon text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/>
</svg>
</div>
}
<h6 class="card-title mb-0">{{title}}</h6>
</div>
<ng-container *ngIf="loading">
@if (loading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</ng-container>
}
<ng-content select ="[header-buttons]"></ng-content>
</div>