Chore: Update Angular to v17 (#4980)

This commit is contained in:
shamoon 2023-12-19 15:02:05 -08:00 committed by GitHub
parent b1f6f52486
commit a6248bec2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 7450 additions and 6557 deletions

View File

@ -94,7 +94,7 @@ The following files need to be changed:
- src-ui/angular.json (under the _projects/paperless-ui/i18n/locales_ JSON key) - src-ui/angular.json (under the _projects/paperless-ui/i18n/locales_ JSON key)
- src/paperless/settings.py (in the _LANGUAGES_ array) - src/paperless/settings.py (in the _LANGUAGES_ array)
- src-ui/src/app/services/settings.service.ts (inside the _getLanguageOptions_ method) - src-ui/src/app/services/settings.service.ts (inside the _LANGUAGE_OPTIONS_ array)
- src-ui/src/app/app.module.ts (import locale from _angular/common/locales_ and call _registerLocaleData_) - src-ui/src/app/app.module.ts (import locale from _angular/common/locales_ and call _registerLocaleData_)
Please add the language in the correct order, alphabetically by locale. Please add the language in the correct order, alphabetically by locale.

View File

@ -12,7 +12,7 @@ COPY ./src-ui /src/src-ui
WORKDIR /src/src-ui WORKDIR /src/src-ui
RUN set -eux \ RUN set -eux \
&& npm update npm -g \ && npm update npm -g \
&& npm ci --omit=optional && npm ci
RUN set -eux \ RUN set -eux \
&& ./node_modules/.bin/ng build --configuration production && ./node_modules/.bin/ng build --configuration production

View File

@ -277,27 +277,17 @@ Adding new languages requires adding the translated files in the
} }
``` ```
2. Add the language to the available options in 2. Add the language to the `LANGUAGE_OPTIONS` array in
`src-ui/src/app/services/settings.service.ts`: `src-ui/src/app/services/settings.service.ts`:
```typescript
getLanguageOptions(): LanguageOption[] {
return [
{code: "en-us", name: $localize`English (US)`, englishName: "English (US)", dateInputFormat: "mm/dd/yyyy"},
{code: "en-gb", name: $localize`English (GB)`, englishName: "English (GB)", dateInputFormat: "dd/mm/yyyy"},
{code: "de", name: $localize`German`, englishName: "German", dateInputFormat: "dd.mm.yyyy"},
{code: "nl", name: $localize`Dutch`, englishName: "Dutch", dateInputFormat: "dd-mm-yyyy"},
{code: "fr", name: $localize`French`, englishName: "French", dateInputFormat: "dd/mm/yyyy"},
{code: "pt-br", name: $localize`Portuguese (Brazil)`, englishName: "Portuguese (Brazil)", dateInputFormat: "dd/mm/yyyy"}
// Add your new language here
]
}
``` ```
`dateInputFormat` is a special string that defines the behavior of `dateInputFormat` is a special string that defines the behavior of
the date input fields and absolutely needs to contain "dd", "mm" the date input fields and absolutely needs to contain "dd", "mm"
and "yyyy". and "yyyy".
```
3. Import and register the Angular data for this locale in 3. Import and register the Angular data for this locale in
`src-ui/src/app/app.module.ts`: `src-ui/src/app/app.module.ts`:

View File

@ -125,18 +125,18 @@
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"options": { "options": {
"browserTarget": "paperless-ui:build:en-US" "buildTarget": "paperless-ui:build:en-US"
}, },
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "paperless-ui:build:production" "buildTarget": "paperless-ui:build:production"
} }
} }
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "paperless-ui:build" "buildTarget": "paperless-ui:build"
} }
}, },
"test": { "test": {

7697
src-ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,56 +11,56 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^16.2.11", "@angular/cdk": "^17.0.4",
"@angular/common": "~16.2.11", "@angular/common": "~17.0.7",
"@angular/compiler": "~16.2.11", "@angular/compiler": "~17.0.7",
"@angular/core": "~16.2.11", "@angular/core": "~17.0.7",
"@angular/forms": "~16.2.11", "@angular/forms": "~17.0.7",
"@angular/localize": "~16.2.11", "@angular/localize": "~17.0.7",
"@angular/platform-browser": "~16.2.11", "@angular/platform-browser": "~17.0.7",
"@angular/platform-browser-dynamic": "~16.2.11", "@angular/platform-browser-dynamic": "~17.0.7",
"@angular/router": "~16.2.11", "@angular/router": "~17.0.7",
"@ng-bootstrap/ng-bootstrap": "^15.1.2", "@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@ng-select/ng-select": "^11.2.0", "@ng-select/ng-select": "^12.0.4",
"@ngneat/dirty-check-forms": "^3.0.3", "@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"mime-names": "^1.0.0", "mime-names": "^1.0.0",
"ngx-color": "^9.0.0", "ngx-color": "^9.0.0",
"ngx-cookie-service": "^16.0.1", "ngx-cookie-service": "^17.0.1",
"ngx-file-drop": "^16.0.0", "ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^13.0.6", "ngx-ui-tour-ng-bootstrap": "^14.0.1",
"pdfjs-dist": "^3.11.174", "pdfjs-dist": "^3.11.174",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"zone.js": "^0.13.3" "zone.js": "^0.14.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/jest": "16.0.1", "@angular-builders/jest": "17.0.0",
"@angular-devkit/build-angular": "~16.2.9", "@angular-devkit/build-angular": "~17.0.7",
"@angular-eslint/builder": "16.2.0", "@angular-eslint/builder": "17.1.1",
"@angular-eslint/eslint-plugin": "16.2.0", "@angular-eslint/eslint-plugin": "17.1.1",
"@angular-eslint/eslint-plugin-template": "16.2.0", "@angular-eslint/eslint-plugin-template": "17.1.1",
"@angular-eslint/schematics": "16.2.0", "@angular-eslint/schematics": "17.1.1",
"@angular-eslint/template-parser": "16.2.0", "@angular-eslint/template-parser": "17.1.1",
"@angular/cli": "~16.2.9", "@angular/cli": "~17.0.7",
"@angular/compiler-cli": "~16.2.3", "@angular/compiler-cli": "~17.0.7",
"@playwright/test": "^1.40.1", "@playwright/test": "^1.40.1",
"@types/jest": "^29.5.10", "@types/jest": "^29.5.10",
"@types/node": "^20.10.2", "@types/node": "^20.10.2",
"@typescript-eslint/eslint-plugin": "^6.13.1", "@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.13.1", "@typescript-eslint/parser": "^6.10.0",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"eslint": "^8.55.0", "eslint": "^8.53.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jest-preset-angular": "^13.1.4", "jest-preset-angular": "^13.1.4",
"jest-websocket-mock": "^2.5.0", "jest-websocket-mock": "^2.5.0",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"ts-node": "~10.9.1", "ts-node": "~10.9.1",
"typescript": "^5.1.6", "typescript": "^5.2.2",
"wait-on": "^7.2.0" "wait-on": "^7.2.0"
} }
} }

View File

@ -19,12 +19,16 @@
</button> </button>
</div> </div>
<div class="btn-group btn-group-sm align-self-end" role="group" aria-label="Previous / Next"> <div class="btn-group btn-group-sm align-self-end" role="group" aria-label="Previous / Next">
<button *ngIf="tourService.hasPrev(step)" class="btn btn-outline-primary" (click)="tourService.prev()"> @if (tourService.hasPrev(step)) {
<button class="btn btn-outline-primary" (click)="tourService.prev()">
« {{ step?.prevBtnTitle }} « {{ step?.prevBtnTitle }}
</button> </button>
<button *ngIf="tourService.hasNext(step)" class="btn btn-outline-primary" (click)="tourService.next()"> }
@if (tourService.hasNext(step)) {
<button class="btn btn-outline-primary" (click)="tourService.next()">
{{ step?.nextBtnTitle }} » {{ step?.nextBtnTitle }} »
</button> </button>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,25 +6,35 @@
</pngx-page-header> </pngx-page-header>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeLog" (activeIdChange)="reloadLogs()" class="nav-tabs"> <ul ngbNav #nav="ngbNav" [(activeId)]="activeLog" (activeIdChange)="reloadLogs()" class="nav-tabs">
<li *ngFor="let logFile of logFiles" [ngbNavItem]="logFile"> @for (logFile of logFiles; track logFile) {
<li [ngbNavItem]="logFile">
<a ngbNavLink> <a ngbNavLink>
{{logFile}}.log {{logFile}}.log
</a> </a>
</li> </li>
<div *ngIf="isLoading || !logFiles.length" class="ps-2 d-flex align-items-center"> }
@if (isLoading || !logFiles.length) {
<div class="ps-2 d-flex align-items-center">
<div class="spinner-border spinner-border-sm me-2" role="status"></div> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container *ngIf="!logFiles.length" i18n>Loading...</ng-container> @if (!logFiles.length) {
<ng-container i18n>Loading...</ng-container>
}
</div> </div>
}
</ul> </ul>
<div [ngbNavOutlet]="nav" class="mt-2"></div> <div [ngbNavOutlet]="nav" class="mt-2"></div>
<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer> <div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
<div *ngIf="isLoading && logFiles.length"> @if (isLoading && logFiles.length) {
<div>
<div class="spinner-border spinner-border-sm me-2" role="status"></div> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container> <ng-container i18n>Loading...</ng-container>
</div> </div>
}
@for (log of logs; track log) {
<p <p
class="m-0 p-0 log-entry-{{getLogLevel(log)}}" class="m-0 p-0 log-entry-{{getLogLevel(log)}}"
*ngFor="let log of logs">{{log}}</p> >{{log}}</p>
}
</div> </div>

View File

@ -24,10 +24,16 @@
<div class="col"> <div class="col">
<select class="form-select" formControlName="displayLanguage"> <select class="form-select" formControlName="displayLanguage">
<option *ngFor="let lang of displayLanguageOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code && currentLocale !== 'en-US'"> - {{lang.englishName}}</span></option> @for (lang of displayLanguageOptions; track lang) {
<option [ngValue]="lang.code">{{lang.name}}@if (lang.code && currentLocale !== 'en-US') {
<span> - {{lang.englishName}}</span>
}</option>
}
</select> </select>
<small *ngIf="displayLanguageIsDirty" class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small> @if (displayLanguageIsDirty) {
<small class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small>
}
</div> </div>
</div> </div>
@ -39,7 +45,11 @@
<div class="col"> <div class="col">
<select class="form-select" formControlName="dateLocale"> <select class="form-select" formControlName="dateLocale">
<option *ngFor="let lang of dateLocaleOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code"> - {{today | customDate:'shortDate':null:lang.code}}</span></option> @for (lang of dateLocaleOptions; track lang) {
<option [ngValue]="lang.code">{{lang.name}}@if (lang.code) {
<span> - {{today | customDate:'shortDate':null:lang.code}}</span>
}</option>
}
</select> </select>
</div> </div>
@ -281,12 +291,12 @@
<h4 i18n>Views</h4> <h4 i18n>Views</h4>
<div formGroupName="savedViews"> <div formGroupName="savedViews">
<div *ngFor="let view of savedViews" [formGroupName]="view.id" class="row"> @for (view of savedViews; track view) {
<div [formGroupName]="view.id" class="row">
<div class="mb-3 col"> <div class="mb-3 col">
<label class="form-label" for="name_{{view.id}}" i18n>Name</label> <label class="form-label" for="name_{{view.id}}" i18n>Name</label>
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}"> <input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
</div> </div>
<div class="mb-2 col"> <div class="mb-2 col">
<label class="form-label" for="show_on_dashboard_{{view.id}}" i18n>&nbsp;<span class="visually-hidden">Appears on</span></label> <label class="form-label" for="show_on_dashboard_{{view.id}}" i18n>&nbsp;<span class="visually-hidden">Appears on</span></label>
<div class="form-check form-switch"> <div class="form-check form-switch">
@ -298,19 +308,23 @@
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label> <label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
</div> </div>
</div> </div>
<div class="mb-2 col-auto"> <div class="mb-2 col-auto">
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label> <label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button> <button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button>
</div> </div>
</div> </div>
}
<div *ngIf="savedViews && savedViews.length === 0" i18n>No saved views defined.</div> @if (savedViews && savedViews.length === 0) {
<div i18n>No saved views defined.</div>
}
<div *ngIf="!savedViews"> @if (!savedViews) {
<div>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div> <div class="visually-hidden" i18n>Loading...</div>
</div> </div>
}
</div> </div>

View File

@ -48,6 +48,12 @@ enum SettingsNavIDs {
SavedViews = 4, SavedViews = 4,
} }
const systemLanguage = { code: '', name: $localize`Use system language` }
const systemDateFormat = {
code: '',
name: $localize`Use date format of display language`,
}
@Component({ @Component({
selector: 'pngx-settings', selector: 'pngx-settings',
templateUrl: './settings.component.html', templateUrl: './settings.component.html',
@ -512,15 +518,11 @@ export class SettingsComponent
} }
get displayLanguageOptions(): LanguageOption[] { get displayLanguageOptions(): LanguageOption[] {
return [{ code: '', name: $localize`Use system language` }].concat( return [systemLanguage].concat(this.settings.getLanguageOptions())
this.settings.getLanguageOptions()
)
} }
get dateLocaleOptions(): LanguageOption[] { get dateLocaleOptions(): LanguageOption[] {
return [ return [systemDateFormat].concat(this.settings.getDateLocaleOptions())
{ code: '', name: $localize`Use date format of display language` },
].concat(this.settings.getDateLocaleOptions())
} }
get today() { get today() {

View File

@ -17,10 +17,10 @@
</div> </div>
</pngx-page-header> </pngx-page-header>
<ng-container *ngIf="!tasksService.completedFileTasks && tasksService.loading"> @if (!tasksService.completedFileTasks && tasksService.loading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div> <div class="visually-hidden" i18n>Loading...</div>
</ng-container> }
<ng-template let-tasks="tasks" #tasksTemplate> <ng-template let-tasks="tasks" #tasksTemplate>
<table class="table table-striped align-middle border shadow-sm"> <table class="table table-striped align-middle border shadow-sm">
@ -34,13 +34,15 @@
</th> </th>
<th scope="col" i18n>Name</th> <th scope="col" i18n>Name</th>
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th> <th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
<th scope="col" class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'" i18n>Results</th> @if (activeTab !== 'started' && activeTab !== 'queued') {
<th scope="col" class="d-none d-lg-table-cell" i18n>Results</th>
}
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th> <th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
<th scope="col" i18n>Actions</th> <th scope="col" i18n>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<ng-container *ngFor="let task of tasks | slice: (page-1) * pageSize : page * pageSize"> @for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) {
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();"> <tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<td> <td>
<div class="form-check"> <div class="form-check">
@ -50,17 +52,27 @@
</td> </td>
<td class="overflow-auto name-col">{{ task.task_file_name }}</td> <td class="overflow-auto name-col">{{ task.task_file_name }}</td>
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td> <td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
<td class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'"> @if (activeTab !== 'started' && activeTab !== 'queued') {
<div *ngIf="task.result?.length > 50" class="result" (click)="expandTask(task); $event.stopPropagation();" <td class="d-none d-lg-table-cell">
@if (task.result?.length > 50) {
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body"> [ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}&hellip;</span> <span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}&hellip;</span>
</div> </div>
<span *ngIf="task.result?.length <= 50" class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span> }
@if (task.result?.length <= 50) {
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span>
}
<ng-template #resultPopover> <ng-template #resultPopover>
<pre class="small mb-0">{{ task.result | slice:0:300 }}<ng-container *ngIf="task.result.length > 300">&hellip;</ng-container></pre> <pre class="small mb-0">{{ task.result | slice:0:300 }}@if (task.result.length > 300) {
<ng-container *ngIf="task.result?.length > 300"><br/><em>(<ng-container i18n>click for full output</ng-container>)</em></ng-container> &hellip;
}</pre>
@if (task.result?.length > 300) {
<br/><em>(<ng-container i18n>click for full output</ng-container>)</em>
}
</ng-template> </ng-template>
</td> </td>
}
<td class="d-lg-none"> <td class="d-lg-none">
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();"> <button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
<svg fill="currentColor" class="" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> <svg fill="currentColor" class="" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
@ -76,11 +88,13 @@
</svg>&nbsp;<ng-container i18n>Dismiss</ng-container> </svg>&nbsp;<ng-container i18n>Dismiss</ng-container>
</button> </button>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<button *ngIf="task.related_document" class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();"> @if (task.related_document) {
<button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/> <use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg>&nbsp;<ng-container i18n>Open Document</ng-container> </svg>&nbsp;<ng-container i18n>Open Document</ng-container>
</button> </button>
}
</ng-container> </ng-container>
</div> </div>
</td> </td>
@ -90,37 +104,49 @@
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre> <pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre>
</td> </td>
</tr> </tr>
</ng-container> }
</tbody> </tbody>
</table> </table>
<div class="pb-3 d-sm-flex justify-content-between align-items-center"> <div class="pb-3 d-sm-flex justify-content-between align-items-center">
<div class="pb-2 pb-sm-0" i18n *ngIf="tasks.length > 0">{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</div> @if (tasks.length > 0) {
<ngb-pagination *ngIf="tasks.length > pageSize" [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination> <div class="pb-2 pb-sm-0" i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</div>
}
@if (tasks.length > pageSize) {
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
}
</div> </div>
</ng-template> </ng-template>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)"> <ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
<li ngbNavItem="failed"> <li ngbNavItem="failed">
<a ngbNavLink i18n>Failed<span *ngIf="tasksService.failedFileTasks.length > 0" class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></a> <a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
</ng-template> </ng-template>
</li> </li>
<li ngbNavItem="completed"> <li ngbNavItem="completed">
<a ngbNavLink i18n>Complete<span *ngIf="tasksService.completedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span></a> <a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
</ng-template> </ng-template>
</li> </li>
<li ngbNavItem="started"> <li ngbNavItem="started">
<a ngbNavLink i18n>Started<span *ngIf="tasksService.startedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span></a> <a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
</ng-template> </ng-template>
</li> </li>
<li ngbNavItem="queued"> <li ngbNavItem="queued">
<a ngbNavLink i18n>Queued<span *ngIf="tasksService.queuedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span></a> <a ngbNavLink i18n>Queued@if (tasksService.queuedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
</ng-template> </ng-template>

View File

@ -1,7 +1,7 @@
<pngx-page-header title="Users & Groups" i18n-title> <pngx-page-header title="Users & Groups" i18n-title>
</pngx-page-header> </pngx-page-header>
<ng-container *ngIf="users"> @if (users) {
<h4 class="d-flex"> <h4 class="d-flex">
<ng-container i18n>Users</ng-container> <ng-container i18n>Users</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }"> <button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
@ -20,8 +20,8 @@
<div class="col" i18n>Actions</div> <div class="col" i18n>Actions</div>
</div> </div>
</li> </li>
@for (user of users; track user) {
<li *ngFor="let user of users" class="list-group-item"> <li class="list-group-item">
<div class="row"> <div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editUser(user)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.User)">{{user.username}}</button></div> <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editUser(user)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.User)">{{user.username}}</button></div>
<div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div> <div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
@ -42,10 +42,11 @@
</div> </div>
</div> </div>
</li> </li>
}
</ul> </ul>
</ng-container> }
<ng-container *ngIf="groups"> @if (groups) {
<h4 class="mt-4 d-flex"> <h4 class="mt-4 d-flex">
<ng-container i18n>Groups</ng-container> <ng-container i18n>Groups</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }"> <button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
@ -55,7 +56,8 @@
<ng-container i18n>Add Group</ng-container> <ng-container i18n>Add Group</ng-container>
</button> </button>
</h4> </h4>
<ul *ngIf="groups.length > 0" class="list-group"> @if (groups.length > 0) {
<ul class="list-group">
<li class="list-group-item"> <li class="list-group-item">
<div class="row"> <div class="row">
<div class="col" i18n>Name</div> <div class="col" i18n>Name</div>
@ -64,8 +66,8 @@
<div class="col" i18n>Actions</div> <div class="col" i18n>Actions</div>
</div> </div>
</li> </li>
@for (group of groups; track group) {
<li *ngFor="let group of groups" class="list-group-item"> <li class="list-group-item">
<div class="row"> <div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div> <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div>
<div class="col"></div> <div class="col"></div>
@ -86,12 +88,17 @@
</div> </div>
</div> </div>
</li> </li>
<li *ngIf="groups.length === 0" class="list-group-item" i18n>No groups defined</li> }
@if (groups.length === 0) {
<li class="list-group-item" i18n>No groups defined</li>
}
</ul> </ul>
}
}
</ng-container> @if (!users || !groups) {
<div>
<div *ngIf="!users || !groups">
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div> <div class="visually-hidden" i18n>Loading...</div>
</div> </div>
}

View File

@ -17,11 +17,13 @@
</svg> </svg>
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search" <input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)" (selectItem)="itemSelected($event)" i18n-placeholder> [formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)" (selectItem)="itemSelected($event)" i18n-placeholder>
<button type="button" *ngIf="!searchFieldEmpty" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()"> @if (!searchFieldEmpty) {
<button type="button" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
<svg fill="currentColor" class="buttonicon-sm me-1"> <svg fill="currentColor" class="buttonicon-sm me-1">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
</button> </button>
}
</form> </form>
</div> </div>
<ul ngbNav class="order-sm-3"> <ul ngbNav class="order-sm-3">
@ -70,8 +72,12 @@
<nav id="sidebarMenu" class="d-md-block bg-light sidebar collapse" [ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating" [ngbCollapse]="isMenuCollapsed"> <nav id="sidebarMenu" class="d-md-block bg-light sidebar collapse" [ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating" [ngbCollapse]="isMenuCollapsed">
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()"> <button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
<svg class="sidebaricon-sm" fill="currentColor"> <svg class="sidebaricon-sm" fill="currentColor">
<use *ngIf="slimSidebarEnabled" xlink:href="assets/bootstrap-icons.svg#chevron-double-right"/> @if (slimSidebarEnabled) {
<use *ngIf="!slimSidebarEnabled" xlink:href="assets/bootstrap-icons.svg#chevron-double-left"/> <use xlink:href="assets/bootstrap-icons.svg#chevron-double-right"/>
}
@if (!slimSidebarEnabled) {
<use xlink:href="assets/bootstrap-icons.svg#chevron-double-left"/>
}
</svg> </svg>
</button> </button>
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around"> <div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
@ -92,12 +98,17 @@
</li> </li>
</ul> </ul>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }"> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews?.length > 0'> @if (savedViewService.loading || savedViewService.sidebarViews?.length > 0) {
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted">
<span i18n>Saved views</span> <span i18n>Saved views</span>
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div> @if (savedViewService.loading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
}
</h6> </h6>
}
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)"> <ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews" @for (view of savedViewService.sidebarViews; track view) {
<li class="nav-item w-100"
cdkDrag cdkDrag
[cdkDragDisabled]="!settingsService.organizingSidebarSavedViews" [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
cdkDragPreviewContainer="parent" cdkDragPreviewContainer="parent"
@ -109,21 +120,27 @@
<use xlink:href="assets/bootstrap-icons.svg#funnel"/> <use xlink:href="assets/bootstrap-icons.svg#funnel"/>
</svg><span>&nbsp;{{view.name}}</span> </svg><span>&nbsp;{{view.name}}</span>
</a> </a>
<div *ngIf="settingsService.organizingSidebarSavedViews" class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle> @if (settingsService.organizingSidebarSavedViews) {
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
<svg class="sidebaricon text-muted" fill="currentColor"> <svg class="sidebaricon text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/> <use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/>
</svg> </svg>
</div> </div>
}
</li> </li>
}
</ul> </ul>
</ng-container> </ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted" *ngIf='openDocuments.length > 0'> @if (openDocuments.length > 0) {
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted">
<span i18n>Open documents</span> <span i18n>Open documents</span>
</h6> </h6>
}
<ul class="nav flex-column mb-2"> <ul class="nav flex-column mb-2">
<li class="nav-item w-100" *ngFor='let d of openDocuments'> @for (d of openDocuments; track d) {
<li class="nav-item w-100">
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> <a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/> <use xlink:href="assets/bootstrap-icons.svg#file-text"/>
@ -135,13 +152,16 @@
</span> </span>
</a> </a>
</li> </li>
<li class="nav-item w-100" *ngIf="openDocuments.length >= 1"> }
@if (openDocuments.length >= 1) {
<li class="nav-item w-100">
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> <a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg><span>&nbsp;<ng-container i18n>Close all</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Close all</ng-container></span>
</a> </a>
</li> </li>
}
</ul> </ul>
</ng-container> </ng-container>
@ -220,10 +240,14 @@
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }" tourAnchor="tour.file-tasks"> <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }" tourAnchor="tour.file-tasks">
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()" ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> <a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()" ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<span *ngIf="tasksService.failedFileTasks.length > 0 && slimSidebarEnabled" class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span> @if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
<span class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
}
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#list-task"/> <use xlink:href="assets/bootstrap-icons.svg#list-task"/>
</svg><span>&nbsp;<ng-container i18n>File Tasks<span *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span></ng-container></span> </svg><span>&nbsp;<ng-container i18n>File Tasks@if (tasksService.failedFileTasks.length > 0) {
<span><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span>
}</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }"> <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
@ -247,7 +271,8 @@
{{ versionString }} {{ versionString }}
</a> </a>
</div> </div>
<div *ngIf="!settingsService.updateCheckingIsSet || appRemoteVersion" class="version-check"> @if (!settingsService.updateCheckingIsSet || appRemoteVersion) {
<div class="version-check">
<ng-template #updateAvailablePopContent> <ng-template #updateAvailablePopContent>
<span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span> <span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span>
</ng-template> </ng-template>
@ -265,24 +290,28 @@
</a> </a>
</p> </p>
</ng-template> </ng-template>
<ng-container *ngIf="settingsService.updateCheckingIsSet; else updateCheckNotSet"> @if (settingsService.updateCheckingIsSet) {
<a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases" @if (appRemoteVersion.update_available) {
<a class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases"
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> [ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" /> <use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg> </svg>
<ng-container *ngIf="appRemoteVersion?.update_available" i18n>Update available</ng-container> @if (appRemoteVersion?.update_available) {
<ng-container i18n>Update available</ng-container>
}
</a> </a>
</ng-container> }
<ng-template #updateCheckNotSet> } @else {
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking" <a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter" container="body"> [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter" container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" /> <use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg> </svg>
</a> </a>
</ng-template> }
</div> </div>
}
</div> </div>
</li> </li>
</ul> </ul>

View File

@ -1,9 +1,15 @@
<button *ngIf="active" class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)"> @if (active) {
<svg *ngIf="!isNumbered && selected" width="1em" height="1em" class="check m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <button class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
@if (!isNumbered && selected) {
<svg width="1em" height="1em" class="check m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#check-lg"/> <use xlink:href="assets/bootstrap-icons.svg#check-lg"/>
</svg> </svg>
<div *ngIf="isNumbered" class="number">{{number}}<span class="visually-hidden">selected</span></div> }
@if (isNumbered) {
<div class="number">{{number}}<span class="visually-hidden">selected</span></div>
}
<svg width=".9em" height="1em" class="x m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg width=".9em" height="1em" class="x m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#x-lg"/> <use xlink:href="assets/bootstrap-icons.svg#x-lg"/>
</svg> </svg>
</button> </button>
}

View File

@ -4,8 +4,12 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p *ngIf="messageBold"><b>{{messageBold}}</b></p> @if (messageBold) {
<p class="mb-0" *ngIf="message" [innerHTML]="message | safeHtml"></p> <p><b>{{messageBold}}</b></p>
}
@if (message) {
<p class="mb-0" [innerHTML]="message | safeHtml"></p>
}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>
@ -16,9 +20,13 @@
{{btnCaption}} {{btnCaption}}
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span> <span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
</span> </span>
<ngb-progressbar *ngIf="!confirmButtonEnabled" style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar> @if (!confirmButtonEnabled) {
<ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
}
</button> </button>
<button *ngIf="alternativeBtnCaption" type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled"> @if (alternativeBtnCaption) {
<button type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled">
{{alternativeBtnCaption}} {{alternativeBtnCaption}}
</button> </button>
}
</div> </div>

View File

@ -5,11 +5,14 @@
</button> </button>
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<button *ngFor="let rd of relativeDates" class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.id)"> @for (rd of relativeDates; track rd) {
<button class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.id)">
<div class="selected-icon"> <div class="selected-icon">
<svg *ngIf="relativeDate === rd.id" fill="currentColor" class="buttonicon-sm"> @if (relativeDate === rd.id) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
}
</div> </div>
<div class="d-flex justify-content-between w-100 align-items-center ps-2"> <div class="d-flex justify-content-between w-100 align-items-center ps-2">
<div class="pe-2 pe-lg-4"> <div class="pe-2 pe-lg-4">
@ -22,16 +25,19 @@
</div> </div>
</div> </div>
</button> </button>
}
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> <div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
<div class="mb-2 d-flex flex-row w-100 justify-content-between small"> <div class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div i18n>After</div> <div i18n>After</div>
<a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()"> @if (dateAfter) {
<a class="btn btn-link p-0 m-0" (click)="clearAfter()">
<svg fill="currentColor" class="buttonicon-sm"> <svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
<small i18n>Clear</small> <small i18n>Clear</small>
</a> </a>
}
</div> </div>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
@ -49,12 +55,14 @@
<div class="mb-2 d-flex flex-row w-100 justify-content-between small"> <div class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div i18n>Before</div> <div i18n>Before</div>
<a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()"> @if (dateBefore) {
<a class="btn btn-link p-0 m-0" (click)="clearBefore()">
<svg fill="currentColor" class="buttonicon-sm"> <svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
<small i18n>Clear</small> <small i18n>Clear</small>
</a> </a>
}
</div> </div>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">

View File

@ -86,7 +86,9 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<span class="text-danger" *ngIf="error?.non_field_errors"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span> @if (error?.non_field_errors) {
<span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
}
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div> </div>

View File

@ -8,8 +8,12 @@
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
<pngx-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text> @if (patternRequired) {
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></pngx-input-check> <pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></pngx-input-check>
}
<div *pngxIfOwner="object"> <div *pngxIfOwner="object">
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form> <pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form>

View File

@ -7,7 +7,9 @@
<div class="modal-body"> <div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select> <pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
<small class="d-block mt-n2" *ngIf="typeFieldDisabled" i18n>Data type cannot be changed after a field is created</small> @if (typeFieldDisabled) {
<small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small>
}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>

View File

@ -9,8 +9,12 @@
<div class="col"> <div class="col">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
<pngx-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text> @if (patternRequired) {
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check> <pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
}
</div> </div>
<div *pngxIfOwner="object"> <div *pngxIfOwner="object">

View File

@ -22,13 +22,15 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<div class="m-0 me-2"> <div class="m-0 me-2">
<ngb-alert #testResultAlert *ngIf="testResult" [type]="testResult" class="mb-0 py-2" (closed)="testResult = null">{{testResultMessage}}</ngb-alert> @if (testResult) {
<ngb-alert #testResultAlert [type]="testResult" class="mb-0 py-2" (closed)="testResult = null">{{testResultMessage}}</ngb-alert>
}
</div> </div>
<button type="button" class="btn btn-outline-primary" (click)="test()" [disabled]="networkActive || testActive"> <button type="button" class="btn btn-outline-primary" (click)="test()" [disabled]="networkActive || testActive">
<ng-container *ngIf="testActive"> @if (testActive) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span class="visually-hidden mr-1" i18n>Loading...</span> <span class="visually-hidden mr-1" i18n>Loading...</span>
</ng-container> }
<ng-container i18n>Test</ng-container> <ng-container i18n>Test</ng-container>
</button> </button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>

View File

@ -26,19 +26,25 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<pngx-input-select i18n-title title="Action" [items]="actionOptions" formControlName="action" i18n-hint hint="Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched."></pngx-input-select> <pngx-input-select i18n-title title="Action" [items]="actionOptions" formControlName="action" i18n-hint hint="Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched."></pngx-input-select>
<pngx-input-text i18n-title title="Action parameter" *ngIf="showActionParamField" formControlName="action_parameter" [error]="error?.action_parameter"></pngx-input-text> @if (showActionParamField) {
<pngx-input-text i18n-title title="Action parameter" formControlName="action_parameter" [error]="error?.action_parameter"></pngx-input-text>
}
<p class="small fst-italic mt-5" i18n>Assignments specified here will supersede any consumption templates.</p> <p class="small fst-italic mt-5" i18n>Assignments specified here will supersede any consumption templates.</p>
<pngx-input-select i18n-title title="Assign title from" [items]="metadataTitleOptions" formControlName="assign_title_from"></pngx-input-select> <pngx-input-select i18n-title title="Assign title from" [items]="metadataTitleOptions" formControlName="assign_title_from"></pngx-input-select>
<pngx-input-tags [allowCreate]="false" formControlName="assign_tags"></pngx-input-tags> <pngx-input-tags [allowCreate]="false" formControlName="assign_tags"></pngx-input-tags>
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select> <pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
<pngx-input-select i18n-title title="Assign correspondent from" [items]="metadataCorrespondentOptions" formControlName="assign_correspondent_from"></pngx-input-select> <pngx-input-select i18n-title title="Assign correspondent from" [items]="metadataCorrespondentOptions" formControlName="assign_correspondent_from"></pngx-input-select>
<pngx-input-select *ngIf="showCorrespondentField" i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select> @if (showCorrespondentField) {
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
}
<pngx-input-check i18n-title title="Assign owner from rule" formControlName="assign_owner_from_rule"></pngx-input-check> <pngx-input-check i18n-title title="Assign owner from rule" formControlName="assign_owner_from_rule"></pngx-input-check>
</div> </div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<span class="text-danger" *ngIf="error?.non_field_errors"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span> @if (error?.non_field_errors) {
<span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
}
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div> </div>

View File

@ -9,8 +9,12 @@
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></pngx-input-text> <pngx-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></pngx-input-text>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
<pngx-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text> @if (patternRequired) {
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check> <pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
}
<div *pngxIfOwner="object"> <div *pngxIfOwner="object">
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form> <pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form>

View File

@ -11,8 +11,12 @@
<pngx-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></pngx-input-check> <pngx-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></pngx-input-check>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
<pngx-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text> @if (patternRequired) {
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check> <pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
}
<div *pngxIfOwner="object"> <div *pngxIfOwner="object">
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form> <pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form>

View File

@ -4,13 +4,14 @@
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" /> <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
</svg> </svg>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div> <div class="d-none d-sm-inline">&nbsp;{{title}}</div>
<ng-container *ngIf="!editing && selectionModel.totalCount > 0"> @if (!editing && selectionModel.totalCount > 0) {
<pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge> <pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge>
</ng-container> }
</button> </button>
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}"> <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<div *ngIf="!editing && manyToOne" class="list-group-item d-flex"> @if (!editing && manyToOne) {
<div class="list-group-item d-flex">
<div class="btn-group btn-group-xs flex-fill" role="group"> <div class="btn-group btn-group-xs flex-fill" role="group">
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd_{{name}}" name="logicalOperatorAnd_{{name}}" value="and"> <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd_{{name}}" name="logicalOperatorAnd_{{name}}" value="and">
<label class="btn btn-outline-primary" for="logicalOperatorAnd_{{name}}" i18n>All</label> <label class="btn btn-outline-primary" for="logicalOperatorAnd_{{name}}" i18n>All</label>
@ -18,7 +19,9 @@
<label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label> <label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label>
</div> </div>
</div> </div>
<div *ngIf="!editing && !manyToOne" class="list-group-item d-flex"> }
@if (!editing && !manyToOne) {
<div class="list-group-item d-flex">
<div class="btn-group btn-group-xs flex-fill" role="group"> <div class="btn-group btn-group-xs flex-fill" role="group">
<input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionInclude_{{name}}" name="intersectionInclude_{{name}}" value="include"> <input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionInclude_{{name}}" name="intersectionInclude_{{name}}" value="include">
<label class="btn btn-outline-primary" for="intersectionInclude_{{name}}" i18n>Include</label> <label class="btn btn-outline-primary" for="intersectionInclude_{{name}}" i18n>Include</label>
@ -26,27 +29,36 @@
<label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label> <label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label>
</div> </div>
</div> </div>
}
<div class="list-group-item"> <div class="list-group-item">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput> <input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
</div> </div>
</div> </div>
<div *ngIf="selectionModel.items" class="items" #buttonItems> @if (selectionModel.items) {
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText; let i = index"> <div class="items" #buttonItems>
@for (item of selectionModel.itemsSorted | filter: filterText; track item; let i = $index) {
@if (allowSelectNone || item.id) {
<pngx-toggleable-dropdown-button <pngx-toggleable-dropdown-button
*ngIf="allowSelectNone || item.id" [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i - 1)" [disabled]="disabled"> [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i - 1)" [disabled]="disabled">
</pngx-toggleable-dropdown-button> </pngx-toggleable-dropdown-button>
</ng-container> }
}
</div> </div>
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled"> }
@if (editing) {
<button class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small> <small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor"> <svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" /> <use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
</svg> </svg>
</button> </button>
<div *ngIf="!editing && manyToOne" class="list-group-item list-group-item-note pt-1 pb-2"> }
@if (!editing && manyToOne) {
<div class="list-group-item list-group-item-note pt-1 pb-2">
<small i18n>Click again to exclude items.</small> <small i18n>Click again to exclude items.</small>
</div> </div>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,24 +1,29 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="toggleItem($event)" [disabled]="disabled"> <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="toggleItem($event)" [disabled]="disabled">
<div class="selected-icon me-1"> <div class="selected-icon me-1">
<ng-container *ngIf="isChecked()"> @if (isChecked()) {
<svg fill="currentColor" class="buttonicon-sm bi-check"> <svg fill="currentColor" class="buttonicon-sm bi-check">
<use xlink:href="assets/bootstrap-icons.svg#check"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
</ng-container> }
<ng-container *ngIf="isPartiallyChecked()"> @if (isPartiallyChecked()) {
<svg fill="currentColor" class="buttonicon-sm bi-dash"> <svg fill="currentColor" class="buttonicon-sm bi-dash">
<use xlink:href="assets/bootstrap-icons.svg#dash"/> <use xlink:href="assets/bootstrap-icons.svg#dash"/>
</svg> </svg>
</ng-container> }
<ng-container *ngIf="isExcluded()"> @if (isExcluded()) {
<svg fill="currentColor" class="buttonicon-sm bi-x"> <svg fill="currentColor" class="buttonicon-sm bi-x">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
</ng-container> }
</div> </div>
<div class="me-1"> <div class="me-1">
<pngx-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="false"></pngx-tag> @if (isTag) {
<ng-template #displayName><small>{{item.name}}</small></ng-template> <pngx-tag [tag]="item" [clickable]="false"></pngx-tag>
} @else {
<small>{{item.name}}</small>
}
</div> </div>
<div *ngIf="!hideCount" class="badge bg-light text-dark rounded-pill ms-auto me-1">{{count ?? item.document_count}}</div> @if (!hideCount) {
<div class="badge bg-light text-dark rounded-pill ms-auto me-1">{{count ?? item.document_count}}</div>
}
</button> </button>

View File

@ -1,18 +1,26 @@
<div class="mb-3"> <div class="mb-3">
<div class="row"> <div class="row">
<div *ngIf="horizontal" class="d-flex align-items-center position-relative hidden-button-container col-md-3"> @if (horizontal) {
<div class="d-flex align-items-center position-relative hidden-button-container col-md-3">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
}
</div> </div>
}
<div [ngClass]="{'col-md-9': horizontal, 'align-items-center': horizontal, 'd-flex': horizontal}"> <div [ngClass]="{'col-md-9': horizontal, 'align-items-center': horizontal, 'd-flex': horizontal}">
<div class="form-check"> <div class="form-check">
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled"> <input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
<label *ngIf="!horizontal" class="form-check-label" [for]="inputId">{{title}}</label> @if (!horizontal) {
<div *ngIf="hint" class="form-text text-muted">{{hint}}</div> <label class="form-check-label" [for]="inputId">{{title}}</label>
}
@if (hint) {
<div class="form-text text-muted">{{hint}}</div>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,7 @@
<div class="mb-3"> <div class="mb-3">
<label *ngIf="title" [for]="inputId">{{title}}</label> @if (title) {
<label [for]="inputId">{{title}}</label>
}
<div class="input-group" [class.is-invalid]="error"> <div class="input-group" [class.is-invalid]="error">
<span class="input-group-text" [style.background-color]="value">&nbsp;&nbsp;&nbsp;</span> <span class="input-group-text" [style.background-color]="value">&nbsp;&nbsp;&nbsp;</span>
@ -21,7 +23,9 @@
</button> </button>
</div> </div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small> @if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
<div class="invalid-feedback"> <div class="invalid-feedback">
{{error}} {{error}}
</div> </div>

View File

@ -2,11 +2,13 @@
<div class="row"> <div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal"> <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
}
</div> </div>
<div class="position-relative" [class.col-md-9]="horizontal"> <div class="position-relative" [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error"> <div class="input-group" [class.is-invalid]="error">
@ -18,20 +20,26 @@
<use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use> <use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use>
</svg> </svg>
</button> </button>
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ fitlerButtonTitle }}"> @if (showFilter) {
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ filterButtonTitle }}">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" /> <use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg> </svg>
</button> </button>
}
</div> </div>
<div class="invalid-feedback position-absolute top-100" i18n>Invalid date.</div> <div class="invalid-feedback position-absolute top-100" i18n>Invalid date.</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small> @if (hint) {
<small *ngIf="getSuggestions().length > 0"> <small class="form-text text-muted">{{hint}}</small>
}
@if (getSuggestions().length > 0) {
<small>
<span i18n>Suggestions:</span>&nbsp; <span i18n>Suggestions:</span>&nbsp;
<ng-container *ngFor="let s of getSuggestions()"> @for (s of getSuggestions(); track s) {
<a (click)="onSuggestionClick(s)" [routerLink]="[]">{{s}}</a>&nbsp; <a (click)="onSuggestionClick(s)" [routerLink]="[]">{{s}}</a>&nbsp;
</ng-container> }
</small> </small>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,12 +1,16 @@
<div class="mb-3 paperless-input-select" [class.disabled]="disabled"> <div class="mb-3 paperless-input-select" [class.disabled]="disabled">
<div class="row"> <div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal"> <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label *ngIf="title" class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> @if (title) {
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
}
</div> </div>
<div [class.col-md-9]="horizontal"> <div [class.col-md-9]="horizontal">
<div> <div>
@ -44,7 +48,9 @@
</ng-template> </ng-template>
</ng-select> </ng-select>
</div> </div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small> @if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,21 +2,27 @@
<div class="row"> <div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal"> <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
}
</div> </div>
<div class="position-relative" [class.col-md-9]="horizontal"> <div class="position-relative" [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error"> <div class="input-group" [class.is-invalid]="error">
<input #inputField type="number" class="form-control" [step]="step" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled"> <input #inputField type="number" class="form-control" [step]="step" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
<button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="disabled">+1</button> @if (showAdd) {
<button class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="disabled">+1</button>
}
</div> </div>
<div class="invalid-feedback position-absolute top-100"> <div class="invalid-feedback position-absolute top-100">
{{error}} {{error}}
</div> </div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small> @if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,14 +2,18 @@
<label class="form-label" [for]="inputId">{{title}}</label> <label class="form-label" [for]="inputId">{{title}}</label>
<div class="input-group" [class.is-invalid]="error"> <div class="input-group" [class.is-invalid]="error">
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete"> <input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
<button *ngIf="showReveal" type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle"> @if (showReveal) {
<button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#eye" /> <use xlink:href="assets/bootstrap-icons.svg#eye" />
</svg> </svg>
</button> </button>
}
</div> </div>
<div class="invalid-feedback"> <div class="invalid-feedback">
{{error}} {{error}}
</div> </div>
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small> @if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
</div> </div>

View File

@ -1,16 +1,23 @@
<ng-container *ngIf="!accordion"> @if (!accordion) {
<h5 i18n>Permissions</h5> <h5 i18n>Permissions</h5>
<ng-container [ngTemplateOutlet]="permissionsForm"></ng-container> <ng-container [ngTemplateOutlet]="permissionsForm"></ng-container>
</ng-container> }
<ng-container *ngIf="accordion"> @if (accordion) {
<ngb-accordion #acc="ngbAccordion" activeIds=""> <div ngbAccordion activeIds="">
<ngb-panel i18n-title title="Edit Permissions"> <div ngbAccordionItem>
<ng-template ngbPanelContent> <h2 ngbAccordionHeader>
<button ngbAccordionButton i18n>Edit Permissions</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<ng-container [ngTemplateOutlet]="permissionsForm"></ng-container> <ng-container [ngTemplateOutlet]="permissionsForm"></ng-container>
</ng-template> </ng-template>
</ngb-panel> </div>
</ngb-accordion> </div>
</ng-container> </div>
</div>
}
<ng-template #permissionsForm> <ng-template #permissionsForm>
<div [formGroup]="form"> <div [formGroup]="form">

View File

@ -1,12 +1,16 @@
<div class="mb-3 paperless-input-select" [class.disabled]="disabled"> <div class="mb-3 paperless-input-select" [class.disabled]="disabled">
<div class="row"> <div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal"> <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label *ngIf="title" class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> @if (title) {
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
}
</div> </div>
<div [class.col-md-9]="horizontal"> <div [class.col-md-9]="horizontal">
<div [class.input-group]="allowCreateNew || showFilter" [class.is-invalid]="error"> <div [class.input-group]="allowCreateNew || showFilter" [class.is-invalid]="error">
@ -31,27 +35,35 @@
(clear)="clearLastSearchTerm()" (clear)="clearLastSearchTerm()"
(blur)="onBlur()"> (blur)="onBlur()">
</ng-select> </ng-select>
<button *ngIf="allowCreateNew" class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled"> @if (allowCreateNew) {
<button class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" /> <use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg> </svg>
</button> </button>
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" title="{{ filterButtonTitle }}"> }
@if (showFilter) {
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" title="{{ filterButtonTitle }}">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" /> <use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg> </svg>
</button> </button>
}
</div> </div>
<div class="invalid-feedback"> <div class="invalid-feedback">
{{error}} {{error}}
</div> </div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small> @if (hint) {
<small *ngIf="getSuggestions().length > 0"> <small class="form-text text-muted">{{hint}}</small>
}
@if (getSuggestions().length > 0) {
<small>
<span i18n>Suggestions:</span>&nbsp; <span i18n>Suggestions:</span>&nbsp;
<ng-container *ngFor="let s of getSuggestions()"> @for (s of getSuggestions(); track s) {
<a (click)="value = s.id; onChange(value)" [routerLink]="[]">{{s.name}}</a>&nbsp; <a (click)="value = s.id; onChange(value)" [routerLink]="[]">{{s.name}}</a>&nbsp;
</ng-container> }
</small> </small>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -122,7 +122,7 @@ export class SelectComponent extends AbstractInputComponent<number> {
} }
} }
addItem(name: string) { addItem(name: string = null) {
if (name) this.createNew.next(name) if (name) this.createNew.next(name)
else this.createNew.next(this._lastSearchTerm) else this.createNew.next(this._lastSearchTerm)
this.clearLastSearchTerm() this.clearLastSearchTerm()

View File

@ -21,33 +21,45 @@
<svg width="1.2em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg width="1.2em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
<pngx-tag *ngIf="item.id && tags" style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag> @if (item.id && tags) {
<pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>
}
</span> </span>
</ng-template> </ng-template>
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm"> <ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
<div class="tag-wrap"> <div class="tag-wrap">
<pngx-tag *ngIf="item.id && tags" class="me-2" [tag]="getTag(item.id)"></pngx-tag> @if (item.id && tags) {
<pngx-tag class="me-2" [tag]="getTag(item.id)"></pngx-tag>
}
</div> </div>
</ng-template> </ng-template>
</ng-select> </ng-select>
<button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled"> @if (allowCreate) {
<button class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" /> <use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg> </svg>
</button> </button>
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="hasPrivate || this.value === null" i18n-title title="Filter documents with these Tags"> }
@if (showFilter) {
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="hasPrivate || this.value === null" i18n-title title="Filter documents with these Tags">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" /> <use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg> </svg>
</button> </button>
}
</div> </div>
<small class="form-text text-muted" *ngIf="hint">{{hint}}</small> @if (hint) {
<small *ngIf="getSuggestions().length > 0" class="position-absolute top-100"> <small class="form-text text-muted">{{hint}}</small>
}
@if (getSuggestions().length > 0) {
<small class="position-absolute top-100">
<span i18n>Suggestions:</span>&nbsp; <span i18n>Suggestions:</span>&nbsp;
<ng-container *ngFor="let tag of getSuggestions()"> @for (tag of getSuggestions(); track tag) {
<a (click)="addTag(tag.id)" [routerLink]="[]">{{tag?.name}}</a>&nbsp; <a (click)="addTag(tag.id)" [routerLink]="[]">{{tag?.name}}</a>&nbsp;
</ng-container> }
</small> </small>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,9 +1,4 @@
import { import { ComponentFixture, TestBed } from '@angular/core/testing'
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { import {
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
@ -15,7 +10,7 @@ import {
DEFAULT_MATCHING_ALGORITHM, DEFAULT_MATCHING_ALGORITHM,
MATCH_ALL, MATCH_ALL,
} from 'src/app/data/matching-model' } from 'src/app/data/matching-model'
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { HttpClientTestingModule } from '@angular/common/http/testing'
import { of } from 'rxjs' import { of } from 'rxjs'

View File

@ -2,15 +2,19 @@
<div class="row"> <div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal"> <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
}
</div> </div>
<div class="position-relative" [class.col-md-9]="horizontal"> <div class="position-relative" [class.col-md-9]="horizontal">
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete"> <input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small> @if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
<div class="invalid-feedback position-absolute top-100"> <div class="invalid-feedback position-absolute top-100">
{{error}} {{error}}
</div> </div>

View File

@ -2,11 +2,13 @@
<div class="row"> <div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal"> <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> @if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
}
</div> </div>
<div [class.col-md-9]="horizontal"> <div [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error"> <div class="input-group" [class.is-invalid]="error">
@ -20,7 +22,9 @@
{{error}} {{error}}
</div> </div>
</div> </div>
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small> @if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,7 +2,9 @@
<div class="col-md text-truncate"> <div class="col-md text-truncate">
<h3 class="text-truncate" style="line-height: 1.4"> <h3 class="text-truncate" style="line-height: 1.4">
{{title}} {{title}}
<span *ngIf="subTitle" class="h6 mb-0 d-block d-md-inline fw-normal ms-md-3 text-truncate" style="line-height: 1.4">{{subTitle}}</span> @if (subTitle) {
<span class="h6 mb-0 d-block d-md-inline fw-normal ms-md-3 text-truncate" style="line-height: 1.4">{{subTitle}}</span>
}
</h3> </h3>
</div> </div>
<div class="btn-toolbar col col-md-auto"> <div class="btn-toolbar col col-md-auto">

View File

@ -36,15 +36,6 @@ import { PDFSinglePageViewer } from 'pdfjs-dist/web/pdf_viewer'
PDFJS['verbosity'] = PDFJS.VerbosityLevel.ERRORS PDFJS['verbosity'] = PDFJS.VerbosityLevel.ERRORS
// Yea this is a straight hack
declare global {
interface WeakKeyTypes {
symbol: Object
}
type WeakKey = WeakKeyTypes[keyof WeakKeyTypes]
}
export enum RenderTextMode { export enum RenderTextMode {
DISABLED, DISABLED,
ENABLED, ENABLED,

View File

@ -5,7 +5,9 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p *ngIf="!object && message" class="mb-3" [innerHTML]="message | safeHtml"></p> @if (!object && message) {
<p class="mb-3" [innerHTML]="message | safeHtml"></p>
}
<form [formGroup]="form"> <form [formGroup]="form">
<pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form> <pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form>
@ -13,10 +15,10 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<ng-container *ngIf="!buttonsEnabled"> @if (!buttonsEnabled) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span class="visually-hidden" i18n>Loading...</span> <span class="visually-hidden" i18n>Loading...</span>
</ng-container> }
<button type="button" class="btn btn-outline-primary" (click)="cancelClicked()" [disabled]="!buttonsEnabled" i18n>Cancel</button> <button type="button" class="btn btn-outline-primary" (click)="cancelClicked()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
<button type="button" class="btn btn-primary" (click)="confirmClicked.emit(permissions)" [disabled]="!buttonsEnabled" i18n>Confirm</button> <button type="button" class="btn btn-primary" (click)="confirmClicked.emit(permissions)" [disabled]="!buttonsEnabled" i18n>Confirm</button>
</div> </div>

View File

@ -10,9 +10,11 @@
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NONE)" [disabled]="disabled"> <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NONE)" [disabled]="disabled">
<div class="selected-icon me-1"> <div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.NONE" fill="currentColor" class="buttonicon-sm"> @if (selectionModel.ownerFilter === OwnerFilterType.NONE) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
}
</div> </div>
<div class="me-1"> <div class="me-1">
<small i18n>All</small> <small i18n>All</small>
@ -20,9 +22,11 @@
</button> </button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SELF)" [disabled]="disabled"> <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SELF)" [disabled]="disabled">
<div class="selected-icon me-1"> <div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.SELF" fill="currentColor" class="buttonicon-sm"> @if (selectionModel.ownerFilter === OwnerFilterType.SELF) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
}
</div> </div>
<div class="me-1"> <div class="me-1">
<small i18n>My documents</small> <small i18n>My documents</small>
@ -30,9 +34,11 @@
</button> </button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NOT_SELF)" [disabled]="disabled"> <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NOT_SELF)" [disabled]="disabled">
<div class="selected-icon me-1"> <div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.NOT_SELF" fill="currentColor" class="buttonicon-sm"> @if (selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
}
</div> </div>
<div class="me-1"> <div class="me-1">
<small i18n>Shared with me</small> <small i18n>Shared with me</small>
@ -40,9 +46,11 @@
</button> </button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SHARED_BY_ME)" [disabled]="disabled"> <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SHARED_BY_ME)" [disabled]="disabled">
<div class="selected-icon me-1"> <div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME" fill="currentColor" class="buttonicon-sm"> @if (selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
}
</div> </div>
<div class="me-1"> <div class="me-1">
<small i18n>Shared by me</small> <small i18n>Shared by me</small>
@ -50,9 +58,11 @@
</button> </button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled"> <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled">
<div class="selected-icon me-1"> <div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.UNOWNED" fill="currentColor" class="buttonicon-sm"> @if (selectionModel.ownerFilter === OwnerFilterType.UNOWNED) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
}
</div> </div>
<div class="me-1"> <div class="me-1">
<small i18n>Unowned</small> <small i18n>Unowned</small>
@ -60,9 +70,11 @@
</button> </button>
<button *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" [disabled]="disabled"> <button *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" [disabled]="disabled">
<div class="selected-icon me-1"> <div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.OTHERS" fill="currentColor" class="buttonicon-sm"> @if (selectionModel.ownerFilter === OwnerFilterType.OTHERS) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
}
</div> </div>
<div class="me-1 w-100"> <div class="me-1 w-100">
<ng-select <ng-select
@ -81,12 +93,14 @@
</ng-select> </ng-select>
</div> </div>
</button> </button>
<div *ngIf="selectionModel.ownerFilter === OwnerFilterType.NONE || selectionModel.ownerFilter === OwnerFilterType.NOT_SELF" class="list-group-item list-group-item-action d-flex align-items-center p-2 ps-3 border-bottom-0 border-start-0 border-end-0"> @if (selectionModel.ownerFilter === OwnerFilterType.NONE || selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
<div class="list-group-item list-group-item-action d-flex align-items-center p-2 ps-3 border-bottom-0 border-start-0 border-end-0">
<div class="form-check form-switch w-100"> <div class="form-check form-switch w-100">
<input type="checkbox" class="form-check-input" id="hideUnowned" [(ngModel)]="this.selectionModel.hideUnowned" (change)="onChange()" [disabled]="disabled"> <input type="checkbox" class="form-check-input" id="hideUnowned" [(ngModel)]="this.selectionModel.hideUnowned" (change)="onChange()" [disabled]="disabled">
<label class="form-check-label w-100" for="hideUnowned"><small i18n>Hide unowned</small></label> <label class="form-check-label w-100" for="hideUnowned"><small i18n>Hide unowned</small></label>
</div> </div>
</div> </div>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -9,19 +9,23 @@
<div class="col" i18n>Delete</div> <div class="col" i18n>Delete</div>
<div class="col" i18n>View</div> <div class="col" i18n>View</div>
</li> </li>
<li class="list-group-item d-flex" *ngFor="let type of PermissionType | keyvalue" [formGroupName]="type.key"> @for (type of PermissionType | keyvalue; track type) {
<li class="list-group-item d-flex" [formGroupName]="type.key">
<div class="col-3">{{type.key}}:</div> <div class="col-3">{{type.key}}:</div>
<div class="col form-check form-check-inline form-switch" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key)" placement="left" triggers="mouseenter:mouseleave"> <div class="col form-check form-check-inline form-switch" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key)" placement="left" triggers="mouseenter:mouseleave">
<input type="checkbox" class="form-check-input" id="{{type.key}}_all" (change)="toggleAll($event, type.key)" [checked]="typesWithAllActions.has(type.key) || isInherited(type.key)" [attr.disabled]="disabled || isInherited(type.key) ? true : null"> <input type="checkbox" class="form-check-input" id="{{type.key}}_all" (change)="toggleAll($event, type.key)" [checked]="typesWithAllActions.has(type.key) || isInherited(type.key)" [attr.disabled]="disabled || isInherited(type.key) ? true : null">
<label class="form-check-label visually-hidden" for="{{type.key}}_all" i18n>All</label> <label class="form-check-label visually-hidden" for="{{type.key}}_all" i18n>All</label>
</div> </div>
@for (action of PermissionAction | keyvalue; track action) {
<div *ngFor="let action of PermissionAction | keyvalue" class="col form-check form-check-inline" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key, action.key)" placement="left" triggers="mouseenter:mouseleave"> <div class="col form-check form-check-inline" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key, action.key)" placement="left" triggers="mouseenter:mouseleave">
<input type="checkbox" class="form-check-input" id="{{type.key}}_{{action.key}}" formControlName="{{action.key}}"> <input type="checkbox" class="form-check-input" id="{{type.key}}_{{action.key}}" formControlName="{{action.key}}">
<label class="form-check-label visually-hidden" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label> <label class="form-check-label visually-hidden" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label>
</div> </div>
}
</li> </li>
<div *ngIf="error" class="invalid-feedback d-block">{{error}}</div> }
@if (error) {
<div class="invalid-feedback d-block">{{error}}</div>
}
</ul> </ul>
</form> </form>

View File

@ -1,22 +1,28 @@
<div class="preview-popup-container"> <div class="preview-popup-container">
<div *ngIf="error; else noError" class="w-100 h-100 position-relative"> @if (error) {
<div class="w-100 h-100 position-relative">
<p class="fst-italic position-absolute top-50 start-50 translate-middle" i18n>Error loading preview</p> <p class="fst-italic position-absolute top-50 start-50 translate-middle" i18n>Error loading preview</p>
</div> </div>
<ng-template #noError> } @else {
<object *ngIf="renderAsObject; else pngxViewer" [data]="previewURL | safeUrl" width="100%" class="bg-light" [class.p-2]="!isPdf"></object> @if (renderAsObject) {
<ng-template #pngxViewer> <object [data]="previewURL | safeUrl" width="100%" class="bg-light" [class.p-2]="!isPdf"></object>
<div *ngIf="requiresPassword" class="w-100 h-100 position-relative"> } @else {
@if (requiresPassword) {
<div class="w-100 h-100 position-relative">
<svg width="2em" height="2em" fill="currentColor" class="position-absolute top-50 start-50 translate-middle"> <svg width="2em" height="2em" fill="currentColor" class="position-absolute top-50 start-50 translate-middle">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-lock"/> <use xlink:href="assets/bootstrap-icons.svg#file-earmark-lock"/>
</svg> </svg>
</div> </div>
<pngx-pdf-viewer *ngIf="!requiresPassword" }
@if (!requiresPassword) {
<pngx-pdf-viewer
[src]="previewURL" [src]="previewURL"
[original-size]="false" [original-size]="false"
[show-borders]="true" [show-borders]="true"
[show-all]="true" [show-all]="true"
(error)="onError($event)"> (error)="onError($event)">
</pngx-pdf-viewer> </pngx-pdf-viewer>
</ng-template> }
</ng-template> }
}
</div> </div>

View File

@ -34,8 +34,12 @@
<input type="text" class="form-control" formControlName="auth_token" readonly> <input type="text" class="form-control" formControlName="auth_token" readonly>
<button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy"> <button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" /> @if (!copied) {
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" /> <use xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
}
@if (copied) {
<use xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
}
</svg><span class="visually-hidden" i18n>Copy</span> </svg><span class="visually-hidden" i18n>Copy</span>
</button> </button>
<button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token"> <button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token">

View File

@ -7,26 +7,37 @@
</button> </button>
<div ngbDropdownMenu aria-labelledby="shareLinksDropdown" class="shadow share-links-dropdown"> <div ngbDropdownMenu aria-labelledby="shareLinksDropdown" class="shadow share-links-dropdown">
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li *ngIf="!shareLinks || shareLinks.length === 0" class="list-group-item fst-italic small text-center text-secondary" i18n> @if (!shareLinks || shareLinks.length === 0) {
<li class="list-group-item fst-italic small text-center text-secondary" i18n>
No existing links No existing links
</li> </li>
<li class="list-group-item" *ngFor="let link of shareLinks"> }
@for (link of shareLinks; track link) {
<li class="list-group-item">
<div class="input-group input-group-sm w-100"> <div class="input-group input-group-sm w-100">
<input type="text" class="form-control" aria-label="Share link" [value]="getShareUrl(link)" readonly> <input type="text" class="form-control" aria-label="Share link" [value]="getShareUrl(link)" readonly>
<span *ngIf="link.expiration" class="input-group-text"> @if (link.expiration) {
<span class="input-group-text">
{{ getDaysRemaining(link) }} {{ getDaysRemaining(link) }}
</span> </span>
}
<button type="button" class="btn btn-sm btn-outline-primary" (click)="copy(link)"> <button type="button" class="btn btn-sm btn-outline-primary" (click)="copy(link)">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use *ngIf="copied !== link.id" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" /> @if (copied !== link.id) {
<use *ngIf="copied === link.id" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" /> <use xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
}
@if (copied === link.id) {
<use xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
}
</svg><span class="visually-hidden" i18n>Copy</span> </svg><span class="visually-hidden" i18n>Copy</span>
</button> </button>
<button *ngIf="canShare(link)" type="button" class="btn btn-sm btn-outline-primary" (click)="share(link)"> @if (canShare(link)) {
<button type="button" class="btn btn-sm btn-outline-primary" (click)="share(link)">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#box-arrow-up" /> <use xlink:href="assets/bootstrap-icons.svg#box-arrow-up" />
</svg><span class="visually-hidden" i18n>Share</span> </svg><span class="visually-hidden" i18n>Share</span>
</button> </button>
}
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete(link)"> <button type="button" class="btn btn-sm btn-outline-danger" (click)="delete(link)">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
@ -35,6 +46,7 @@
</div> </div>
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied === link.id" i18n>Copied!</span> <span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied === link.id" i18n>Copied!</span>
</li> </li>
}
<li class="list-group-item pt-3 pb-2"> <li class="list-group-item pt-3 pb-2">
<div class="input-group input-group-sm w-100"> <div class="input-group input-group-sm w-100">
<div class="form-check form-switch ms-auto small"> <div class="form-check form-switch ms-auto small">
@ -45,13 +57,19 @@
<div class="input-group input-group-sm w-100 mt-2"> <div class="input-group input-group-sm w-100 mt-2">
<label class="input-group-text" for="addLink"><ng-container i18n>Expires</ng-container>:</label> <label class="input-group-text" for="addLink"><ng-container i18n>Expires</ng-container>:</label>
<select class="form-select form-select-sm" [(ngModel)]="expirationDays"> <select class="form-select form-select-sm" [(ngModel)]="expirationDays">
<option *ngFor="let option of EXPIRATION_OPTIONS" [ngValue]="option.value">{{ option.label }}</option> @for (option of EXPIRATION_OPTIONS; track option) {
<option [ngValue]="option.value">{{ option.label }}</option>
}
</select> </select>
<button class="btn btn-sm btn-outline-primary ms-auto" type="button" (click)="createLink()" [disabled]="loading"> <button class="btn btn-sm btn-outline-primary ms-auto" type="button" (click)="createLink()" [disabled]="loading">
<div *ngIf="loading" class="spinner-border spinner-border-sm me-2" role="status"></div> @if (loading) {
<svg *ngIf="!loading" class="buttonicon me-1" fill="currentColor"> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
}
@if (!loading) {
<svg class="buttonicon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" /> <use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg> </svg>
}
<ng-container i18n>Create</ng-container> <ng-container i18n>Create</ng-container>
</button> </button>
</div> </div>

View File

@ -1,9 +1,15 @@
<ng-container *ngIf="tag !== undefined; else privateTag" > @if (tag !== undefined) {
<span *ngIf="!clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span> @if (!clickable) {
<a [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a> <span class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span>
</ng-container> }
@if (clickable) {
<ng-template #privateTag> <a [title]="linkTitle" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a>
<span *ngIf="!clickable" class="badge private" i18n>Private</span> }
<a [title]="linkTitle" *ngIf="clickable" class="badge private" i18n>Private</a> } @else {
</ng-template> @if (!clickable) {
<span class="badge private" i18n>Private</span>
}
@if (clickable) {
<a [title]="linkTitle" class="badge private" i18n>Private</a>
}
}

View File

@ -1,5 +1,5 @@
@for (toast of toasts; track toast) {
<ngb-toast <ngb-toast
*ngFor="let toast of toasts"
[autohide]="true" [delay]="toast.delay" [autohide]="true" [delay]="toast.delay"
[class]="toast.classname" [class]="toast.classname"
[class.mb-2]="true" [class.mb-2]="true"
@ -9,14 +9,20 @@
<span class="visually-hidden">{{ toast.delayRemaining / 1000 | number: '1.0-0' }} seconds</span> <span class="visually-hidden">{{ toast.delayRemaining / 1000 | number: '1.0-0' }} seconds</span>
<div class="d-flex align-items-top"> <div class="d-flex align-items-top">
<svg class="sidebaricon-sm mt-1 me-2 flex-shrink-0" fill="currentColor"> <svg class="sidebaricon-sm mt-1 me-2 flex-shrink-0" fill="currentColor">
<use *ngIf="!toast.error" xlink:href="assets/bootstrap-icons.svg#info-circle"/> @if (!toast.error) {
<use *ngIf="toast.error" xlink:href="assets/bootstrap-icons.svg#exclamation-triangle"/> <use xlink:href="assets/bootstrap-icons.svg#info-circle"/>
}
@if (toast.error) {
<use xlink:href="assets/bootstrap-icons.svg#exclamation-triangle"/>
}
</svg> </svg>
<div> <div>
<p class="mb-0">{{toast.content}}</p> <p class="mb-0">{{toast.content}}</p>
<details *ngIf="toast.error"> @if (toast.error) {
<details>
<div class="mt-2"> <div class="mt-2">
<dl class="row mb-0" *ngIf="isDetailedError(toast.error)"> @if (isDetailedError(toast.error)) {
<dl class="row mb-0">
<dt class="col-sm-3 fw-normal text-end">URL</dt> <dt class="col-sm-3 fw-normal text-end">URL</dt>
<dd class="col-sm-9">{{ toast.error.url }}</dd> <dd class="col-sm-9">{{ toast.error.url }}</dd>
<dt class="col-sm-3 fw-normal text-end" i18n>Status</dt> <dt class="col-sm-3 fw-normal text-end" i18n>Status</dt>
@ -24,20 +30,29 @@
<dt class="col-sm-3 fw-normal text-end" i18n>Error</dt> <dt class="col-sm-3 fw-normal text-end" i18n>Error</dt>
<dd class="col-sm-9">{{ getErrorText(toast.error) }}</dd> <dd class="col-sm-9">{{ getErrorText(toast.error) }}</dd>
</dl> </dl>
}
<div class="row"> <div class="row">
<div class="col offset-sm-3"> <div class="col offset-sm-3">
<button class="btn btn-sm btn-outline-dark" (click)="copyError(toast.error)"> <button class="btn btn-sm btn-outline-dark" (click)="copyError(toast.error)">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard" /> @if (!copied) {
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check" /> <use xlink:href="assets/bootstrap-icons.svg#clipboard" />
}
@if (copied) {
<use xlink:href="assets/bootstrap-icons.svg#clipboard-check" />
}
</svg>&nbsp;<ng-container i18n>Copy Raw Error</ng-container> </svg>&nbsp;<ng-container i18n>Copy Raw Error</ng-container>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</details> </details>
<p class="mb-0 mt-2" *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p> }
@if (toast.action) {
<p class="mb-0 mt-2"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p>
}
</div> </div>
<button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="toastService.closeToast(toast);"></button> <button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="toastService.closeToast(toast);"></button>
</div> </div>
</ngb-toast> </ngb-toast>
}

View File

@ -10,7 +10,7 @@ import { Clipboard } from '@angular/cdk/clipboard'
}) })
export class ToastsComponent implements OnInit, OnDestroy { export class ToastsComponent implements OnInit, OnDestroy {
constructor( constructor(
private toastService: ToastService, public toastService: ToastService,
private clipboard: Clipboard private clipboard: Clipboard
) {} ) {}

View File

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

View File

@ -5,9 +5,12 @@
[draggable]="savedView" [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"> @if (documents.length) {
<table content class="table table-hover mb-0 align-middle">
<thead> <thead>
<tr> <tr>
<th scope="col" i18n>Created</th> <th scope="col" i18n>Created</th>
@ -17,16 +20,21 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let doc of documents" (mouseleave)="maybeClosePopover()"> @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}}" 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"> <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> <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>
<td class="py-2 py-md-3 d-none d-md-table-cell"> <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> @for (t of doc.tags$ | async; track t) {
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag>
}
</td> </td>
<td class="position-relative py-2 py-md-3 d-none d-md-table-cell"> <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> @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"> <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" <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" [ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle"
@ -46,11 +54,12 @@
</div> </div>
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
} @else {
<ng-template #empty>
<p 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>
</ng-template> }
</pngx-widget-frame> </pngx-widget-frame>

View File

@ -1,10 +1,12 @@
<pngx-widget-frame title="Statistics" [loading]="loading" i18n-title> <pngx-widget-frame title="Statistics" [loading]="loading" i18n-title>
<ng-container content> <ng-container content>
<div class="list-group border-light"> <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()"> @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>: <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> <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>
}
<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/"> <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>: <ng-container i18n>Total documents</ng-container>:
<span class="badge bg-primary rounded-pill">{{statistics?.documents_total}}</span> <span class="badge bg-primary rounded-pill">{{statistics?.documents_total}}</span>
@ -13,10 +15,12 @@
<ng-container i18n>Total characters</ng-container>: <ng-container i18n>Total characters</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.character_count | number}}</span> <span class="badge bg-secondary text-light rounded-pill">{{statistics?.character_count | number}}</span>
</div> </div>
<div *ngIf="statistics?.document_file_type_counts?.length > 1" class="list-group-item filetypes"> @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="d-flex justify-content-between align-items-center my-2">
<div class="progress flex-grow-1"> <div class="progress flex-grow-1">
<div *ngFor="let filetype of statistics?.document_file_type_counts; let i = index; let last = last" @for (filetype of statistics?.document_file_type_counts; track filetype; let i = $index; let last = $last) {
<div
class="progress-bar bg-primary" class="progress-bar bg-primary"
role="progressbar" role="progressbar"
[ngbPopover]="getFileTypeName(filetype)" [ngbPopover]="getFileTypeName(filetype)"
@ -30,43 +34,55 @@
aria-valuemin="0" aria-valuemin="0"
aria-valuemax="100"> aria-valuemax="100">
</div> </div>
}
</div> </div>
</div> </div>
<div class="d-flex flex-wrap align-items-start"> <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"> @for (filetype of statistics?.document_file_type_counts; track filetype; let i = $index) {
<div class="d-flex">
<div class="text-nowrap me-2"> <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> <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> <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> </div>
}
</div> </div>
<div class="list-group border-light mt-3"> <div class="list-group border-light mt-3">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"> <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/"> @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>: <ng-container i18n>Tags</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.tag_count | number}}</span> <span class="badge bg-secondary text-light rounded-pill">{{statistics?.tag_count | number}}</span>
</a> </a>
}
</ng-container> </ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"> <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/"> @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>: <ng-container i18n>Correspondents</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.correspondent_count | number}}</span> <span class="badge bg-secondary text-light rounded-pill">{{statistics?.correspondent_count | number}}</span>
</a> </a>
}
</ng-container> </ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"> <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/"> @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>: <ng-container i18n>Document Types</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.document_type_count | number}}</span> <span class="badge bg-secondary text-light rounded-pill">{{statistics?.document_type_count | number}}</span>
</a> </a>
}
</ng-container> </ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"> <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/"> @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>: <ng-container i18n>Storage Paths</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.storage_path_count | number}}</span> <span class="badge bg-secondary text-light rounded-pill">{{statistics?.storage_path_count | number}}</span>
</a> </a>
}
</ng-container> </ng-container>
</div> </div>
</ng-container> </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="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="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"> <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> @if (getStatus().length > 0) {
<a *ngIf="getStatusCompleted().length > 0" class="btn-link" (click)="dismissCompleted()" [routerLink]="[]" > <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> <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"> <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="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"/> <path d="M5.354 7.146l.896.897-.707.707-.897-.896a.5.5 0 1 1 .708-.708z"/>
</svg> </svg>
</a> </a>
}
</div> </div>
</div> </div>
<div *ngFor="let status of getStatus()"> @for (status of getStatus(); track status) {
<div>
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container> <ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
</div> </div>
}
</div> </div>
<div *ngIf="getStatusHidden().length" class="alerts-hidden"> @if (getStatusHidden().length) {
<p *ngIf="!alertsExpanded" class="mt-3 mb-0 text-center"> <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> <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; &nbsp;&bull;&nbsp;
<a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a> <a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a>
</p> </p>
}
<div #hiddenAlerts="ngbCollapse" [(ngbCollapse)]="!alertsExpanded"> <div #hiddenAlerts="ngbCollapse" [(ngbCollapse)]="!alertsExpanded">
<div *ngFor="let status of getStatusHidden()"> @for (status of getStatusHidden(); track status) {
<div>
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container> <ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
</div> </div>
}
</div> </div>
</div> </div>
}
</div> </div>
</pngx-widget-frame> </pngx-widget-frame>
@ -42,17 +54,23 @@
<div class="col col-lg-4 col-xl-3"> <div class="col col-lg-4 col-xl-3">
<ngb-alert type="secondary" class="mt-2 mb-0" [dismissible]="isFinished(status)" (closed)="dismiss(status)"> <ngb-alert type="secondary" class="mt-2 mb-0" [dismissible]="isFinished(status)" (closed)="dismiss(status)">
<h6 class="alert-heading">{{status.filename}}</h6> <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> <ngb-progressbar [value]="status.getProgress()" [max]="1" [type]="getStatusColor(status)"></ngb-progressbar>
<div *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <div *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<div *ngIf="isFinished(status)"> @if (isFinished(status)) {
<button *ngIf="status.documentId" class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(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> <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"> <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"/> <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> </svg>
</button> </button>
}
</div> </div>
}
</div> </div>
</ngb-alert> </ngb-alert>
</div> </div>

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<pngx-page-header [(title)]="title"> <pngx-page-header [(title)]="title">
<ng-container *ngIf="getContentType() === 'application/pdf' && !useNativePdfViewer"> @if (getContentType() === 'application/pdf' && !useNativePdfViewer) {
<div class="input-group input-group-sm me-2 d-none d-md-flex"> <div class="input-group input-group-sm me-2 d-none d-md-flex">
<div class="input-group-text" i18n>Page</div> <div class="input-group-text" i18n>Page</div>
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" /> <input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
@ -8,13 +8,15 @@
<div class="input-group input-group-sm me-5 d-none d-md-flex"> <div class="input-group input-group-sm me-5 d-none d-md-flex">
<button class="btn btn-outline-secondary" (click)="decreaseZoom()" i18n>-</button> <button class="btn btn-outline-secondary" (click)="decreaseZoom()" i18n>-</button>
<select class="form-select" (change)="onZoomSelect($event)"> <select class="form-select" (change)="onZoomSelect($event)">
<option *ngFor="let setting of zoomSettings" [value]="setting" [selected]="previewZoomSetting === setting"> @for (setting of zoomSettings; track setting) {
<option [value]="setting" [selected]="previewZoomSetting === setting">
{{ getZoomSettingTitle(setting) }} {{ getZoomSettingTitle(setting) }}
</option> </option>
}
</select> </select>
<button class="btn btn-outline-secondary" (click)="increaseZoom()" i18n>+</button> <button class="btn btn-outline-secondary" (click)="increaseZoom()" i18n>+</button>
</div> </div>
</ng-container> }
<button type="button" class="btn btn-sm btn-outline-danger me-4" (click)="delete()" [disabled]="!userIsOwner" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }"> <button type="button" class="btn btn-sm btn-outline-danger me-4" (click)="delete()" [disabled]="!userIsOwner" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
@ -29,12 +31,14 @@
</svg><span class="d-none d-lg-inline ps-1" i18n>Download</span> </svg><span class="d-none d-lg-inline ps-1" i18n>Download</span>
</a> </a>
<div class="btn-group" ngbDropdown role="group" *ngIf="metadata?.has_archive_version"> @if (metadata?.has_archive_version) {
<div class="btn-group" ngbDropdown role="group">
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle></button> <button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle></button>
<div class="dropdown-menu shadow" ngbDropdownMenu> <div class="dropdown-menu shadow" ngbDropdownMenu>
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a> <a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
</div> </div>
</div> </div>
}
</div> </div>
<div class="ms-auto" ngbDropdown> <div class="ms-auto" ngbDropdown>
@ -115,18 +119,36 @@
<pngx-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" <pngx-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
(createNew)="createStoragePath($event)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"></pngx-input-select> (createNew)="createStoragePath($event)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"></pngx-input-select>
<pngx-input-tags formControlName="tags" [suggestions]="suggestions?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags> <pngx-input-tags formControlName="tags" [suggestions]="suggestions?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags>
<ng-container *ngFor="let fieldInstance of document?.custom_fields; let i = index"> @for (fieldInstance of document?.custom_fields; track fieldInstance; let i = $index) {
<div [formGroup]="customFieldFormFields.controls[i]" [ngSwitch]="getCustomFieldFromInstance(fieldInstance)?.data_type"> <div [formGroup]="customFieldFormFields.controls[i]">
<pngx-input-text *ngSwitchCase="PaperlessCustomFieldDataType.String" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-text> @switch (getCustomFieldFromInstance(fieldInstance)?.data_type) {
<pngx-input-date *ngSwitchCase="PaperlessCustomFieldDataType.Date" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-date> @case (PaperlessCustomFieldDataType.String) {
<pngx-input-number *ngSwitchCase="PaperlessCustomFieldDataType.Integer" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [error]="getCustomFieldError(i)"></pngx-input-number> <pngx-input-text formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-text>
<pngx-input-number *ngSwitchCase="PaperlessCustomFieldDataType.Float" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [step]=".1" [error]="getCustomFieldError(i)"></pngx-input-number> }
<pngx-input-number *ngSwitchCase="PaperlessCustomFieldDataType.Monetary" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [step]=".01" [error]="getCustomFieldError(i)"></pngx-input-number> @case (PaperlessCustomFieldDataType.Date) {
<pngx-input-check *ngSwitchCase="PaperlessCustomFieldDataType.Boolean" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true"></pngx-input-check> <pngx-input-date formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-date>
<pngx-input-url *ngSwitchCase="PaperlessCustomFieldDataType.Url" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-url> }
<pngx-input-document-link *ngSwitchCase="PaperlessCustomFieldDataType.DocumentLink" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [parentDocumentID]="documentId" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-document-link> @case (PaperlessCustomFieldDataType.Integer) {
<pngx-input-number formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [error]="getCustomFieldError(i)"></pngx-input-number>
}
@case (PaperlessCustomFieldDataType.Float) {
<pngx-input-number formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [step]=".1" [error]="getCustomFieldError(i)"></pngx-input-number>
}
@case (PaperlessCustomFieldDataType.Monetary) {
<pngx-input-number formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [step]=".01" [error]="getCustomFieldError(i)"></pngx-input-number>
}
@case (PaperlessCustomFieldDataType.Boolean) {
<pngx-input-check formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true"></pngx-input-check>
}
@case (PaperlessCustomFieldDataType.Url) {
<pngx-input-url formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-url>
}
@case (PaperlessCustomFieldDataType.DocumentLink) {
<pngx-input-document-link formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [parentDocumentID]="documentId" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-document-link>
}
}
</div> </div>
</ng-container> }
</div> </div>
<div class="d-flex border-top pt-3"> <div class="d-flex border-top pt-3">
@ -148,7 +170,8 @@
<a ngbNavLink i18n>Metadata</a> <a ngbNavLink i18n>Metadata</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<table class="table table-borderless" *ngIf="document"> @if (document) {
<table class="table table-borderless">
<tbody> <tbody>
<tr> <tr>
<td i18n>Date modified</td> <td i18n>Date modified</td>
@ -178,38 +201,54 @@
<td i18n>Original mime type</td> <td i18n>Original mime type</td>
<td>{{metadata?.original_mime_type}}</td> <td>{{metadata?.original_mime_type}}</td>
</tr> </tr>
<tr *ngIf="metadata?.has_archive_version"> @if (metadata?.has_archive_version) {
<tr>
<td i18n>Archive MD5 checksum</td> <td i18n>Archive MD5 checksum</td>
<td>{{metadata?.archive_checksum}}</td> <td>{{metadata?.archive_checksum}}</td>
</tr> </tr>
<tr *ngIf="metadata?.has_archive_version"> }
@if (metadata?.has_archive_version) {
<tr>
<td i18n>Archive file size</td> <td i18n>Archive file size</td>
<td>{{metadata?.archive_size | fileSize}}</td> <td>{{metadata?.archive_size | fileSize}}</td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
}
<pngx-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata.original_metadata" *ngIf="metadata?.original_metadata?.length > 0"></pngx-metadata-collapse> @if (metadata?.original_metadata?.length > 0) {
<pngx-metadata-collapse i18n-title title="Archived document metadata" [metadata]="metadata.archive_metadata" *ngIf="metadata?.archive_metadata?.length > 0"></pngx-metadata-collapse> <pngx-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata.original_metadata"></pngx-metadata-collapse>
}
@if (metadata?.archive_metadata?.length > 0) {
<pngx-metadata-collapse i18n-title title="Archived document metadata" [metadata]="metadata.archive_metadata"></pngx-metadata-collapse>
}
</ng-template> </ng-template>
</li> </li>
<li [ngbNavItem]="DocumentDetailNavIDs.Preview" class="d-md-none"> <li [ngbNavItem]="DocumentDetailNavIDs.Preview" class="d-md-none">
<a ngbNavLink i18n>Preview</a> <a ngbNavLink i18n>Preview</a>
<ng-template ngbNavContent *ngIf="!pdfPreview.offsetParent"> @if (!pdfPreview.offsetParent) {
<ng-template ngbNavContent>
<ng-container *ngTemplateOutlet="previewContent"></ng-container> <ng-container *ngTemplateOutlet="previewContent"></ng-container>
</ng-template> </ng-template>
}
</li> </li>
<li [ngbNavItem]="DocumentDetailNavIDs.Notes" *ngIf="notesEnabled"> @if (notesEnabled) {
<a class="text-nowrap" ngbNavLink i18n>Notes <span *ngIf="document?.notes.length" class="badge text-bg-secondary ms-1">{{document.notes.length}}</span></a> <li [ngbNavItem]="DocumentDetailNavIDs.Notes">
<a class="text-nowrap" ngbNavLink i18n>Notes @if (document?.notes.length) {
<span class="badge text-bg-secondary ms-1">{{document.notes.length}}</span>
}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<pngx-document-notes [documentId]="documentId" [notes]="document?.notes" [addDisabled]="!userCanEdit" (updated)="notesUpdated($event)"></pngx-document-notes> <pngx-document-notes [documentId]="documentId" [notes]="document?.notes" [addDisabled]="!userCanEdit" (updated)="notesUpdated($event)"></pngx-document-notes>
</ng-template> </ng-template>
</li> </li>
}
<li [ngbNavItem]="DocumentDetailNavIDs.Permissions" *ngIf="showPermissions"> @if (showPermissions) {
<li [ngbNavItem]="DocumentDetailNavIDs.Permissions">
<a ngbNavLink i18n>Permissions</a> <a ngbNavLink i18n>Permissions</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<div class="mb-3"> <div class="mb-3">
@ -217,6 +256,7 @@
</div> </div>
</ng-template> </ng-template>
</li> </li>
}
</ul> </ul>
<div [ngbNavOutlet]="nav" class="mt-3"></div> <div [ngbNavOutlet]="nav" class="mt-3"></div>
@ -226,14 +266,16 @@
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview> <div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview>
<ng-container *ngTemplateOutlet="previewContent"></ng-container> <ng-container *ngTemplateOutlet="previewContent"></ng-container>
<ng-container *ngIf="renderAsPlainText"> @if (renderAsPlainText) {
<div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div> <div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
</ng-container> }
<div *ngIf="requiresPassword" class="password-prompt"> @if (requiresPassword) {
<div class="password-prompt">
<form> <form>
<input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" /> <input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
</form> </form>
</div> </div>
}
</div> </div>
</div> </div>
@ -242,22 +284,29 @@
<div class="btn-group ms-auto"> <div class="btn-group ms-auto">
<ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"> <ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button> <button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button>
<button *ngIf="hasNext()" type="button" class="order-1 btn btn-sm btn-outline-primary" (click)="saveEditNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save &amp; next</button> @if (hasNext()) {
<button *ngIf="!hasNext()" type="button" class="order-2 btn btn-sm btn-outline-primary" (click)="save(true)" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save &amp; close</button> <button type="button" class="order-1 btn btn-sm btn-outline-primary" (click)="saveEditNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save &amp; next</button>
}
@if (!hasNext()) {
<button type="button" class="order-2 btn btn-sm btn-outline-primary" (click)="save(true)" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save &amp; close</button>
}
</ng-container> </ng-container>
<button type="button" class="order-0 btn btn-sm btn-outline-secondary" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button> <button type="button" class="order-0 btn btn-sm btn-outline-secondary" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button>
</div> </div>
</ng-template> </ng-template>
<ng-template #previewContent> <ng-template #previewContent>
<div *ngIf="!metadata" class="w-100 h-100 d-flex align-items-center justify-content-center"> @if (!metadata) {
<div class="w-100 h-100 d-flex align-items-center justify-content-center">
<div> <div>
<div class="spinner-border spinner-border-sm me-2" role="status"></div> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container> <ng-container i18n>Loading...</ng-container>
</div> </div>
</div> </div>
<ng-container *ngIf="getContentType() === 'application/pdf'"> }
<div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer"> @if (getContentType() === 'application/pdf') {
@if (!useNativePdfViewer ) {
<div class="preview-sticky pdf-viewer-container">
<pngx-pdf-viewer <pngx-pdf-viewer
[src]="{ url: previewUrl, password: password }" [src]="{ url: previewUrl, password: password }"
[original-size]="false" [original-size]="false"
@ -270,16 +319,18 @@
(after-load-complete)="pdfPreviewLoaded($event)"> (after-load-complete)="pdfPreviewLoaded($event)">
</pngx-pdf-viewer> </pngx-pdf-viewer>
</div> </div>
<ng-template #nativePdfViewer> } @else {
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object> <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
</ng-template> }
</ng-container> }
<ng-container *ngIf="renderAsPlainText"> @if (renderAsPlainText) {
<div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div> <div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
</ng-container> }
<div *ngIf="showPasswordField" class="password-prompt"> @if (showPasswordField) {
<div class="password-prompt">
<form> <form>
<input autocomplete="" autofocus="true" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" /> <input autocomplete="" autofocus="true" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
</form> </form>
</div> </div>
}
</ng-template> </ng-template>

View File

@ -1,12 +1,16 @@
<h6> <h6>
<button type="button" class="btn btn-outline-secondary btn-sm me-2" <button type="button" class="btn btn-outline-secondary btn-sm me-2"
(click)="expand = !expand"> (click)="expand = !expand">
<svg class="buttonicon" fill="currentColor" *ngIf="!expand"> @if (!expand) {
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#caret-down" /> <use xlink:href="assets/bootstrap-icons.svg#caret-down" />
</svg> </svg>
<svg class="buttonicon" fill="currentColor" *ngIf="expand"> }
@if (expand) {
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#caret-up" /> <use xlink:href="assets/bootstrap-icons.svg#caret-up" />
</svg> </svg>
}
</button> </button>
{{title}} {{title}}
</h6> </h6>
@ -14,10 +18,12 @@
<div #collapse="ngbCollapse" [(ngbCollapse)]="!expand"> <div #collapse="ngbCollapse" [(ngbCollapse)]="!expand">
<table class="table table-borderless"> <table class="table table-borderless">
<tbody> <tbody>
<tr *ngFor="let m of metadata"> @for (m of metadata; track m) {
<tr>
<td>{{m.prefix}}:{{m.key}}</td> <td>{{m.prefix}}:{{m.key}}</td>
<td class="metadata-column">{{m.value}}</td> <td class="metadata-column">{{m.value}}</td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -93,12 +93,16 @@
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()"> <button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
<svg *ngIf="!awaitingDownload" class="toolbaricon" fill="currentColor"> @if (!awaitingDownload) {
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-down" /> <use xlink:href="assets/bootstrap-icons.svg#arrow-down" />
</svg> </svg>
<div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status"> }
@if (awaitingDownload) {
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Preparing download...</span> <span class="visually-hidden">Preparing download...</span>
</div> </div>
}
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Download</ng-container></div> <div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Download</ng-container></div>
</button> </button>
<div ngbDropdown class="me-2 d-flex btn-group" role="group"> <div ngbDropdown class="me-2 d-flex btn-group" role="group">

View File

@ -16,23 +16,35 @@
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h5 class="card-title"> <h5 class="card-title">
<ng-container *ngIf="document.correspondent"> @if (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> @if (clickCorrespondent.observers.length ) {
<ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>: <a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>
</ng-container> } @else {
{{(document.correspondent$ | async)?.name}}
}
:
}
{{document.title | documentTitle}} {{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> </h5>
</div> </div>
<p class="card-text"> <p class="card-text">
<span *ngIf="document.__search_hit__ && document.__search_hit__.highlights" [innerHtml]="document.__search_hit__.highlights"></span> @if (document.__search_hit__ && document.__search_hit__.highlights) {
<span *ngFor="let highlight of searchNoteHighlights" class="d-block"> <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"> <svg width="1em" height="1em" fill="currentColor" class="me-2">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
</svg> </svg>
<span [innerHtml]="highlight"></span> <span [innerHtml]="highlight"></span>
</span> </span>
<span *ngIf="!document.__search_hit__" class="result-content">{{contentTrimmed}}</span> }
@if (!document.__search_hit__) {
<span class="result-content">{{contentTrimmed}}</span>
}
</p> </p>
@ -67,32 +79,40 @@
<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">
<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> @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"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
</svg> </svg>
<small i18n>{{document.notes.length}} Notes</small> <small i18n>{{document.notes.length}} Notes</small>
</button> </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 }
@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()"> (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark"/> <use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
</svg> </svg>
<small>{{(document.document_type$ | async)?.name}}</small> <small>{{(document.document_type$ | async)?.name}}</small>
</button> </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 }
@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()"> (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#archive"/> <use xlink:href="assets/bootstrap-icons.svg#archive"/>
</svg> </svg>
<small>{{(document.storage_path$ | async)?.name}}</small> <small>{{(document.storage_path$ | async)?.name}}</small>
</button> </button>
<div *ngIf="document.archive_serial_number | isNumber" class="list-group-item me-2 bg-light text-dark p-1 border-0"> }
@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"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#upc-scan"/> <use xlink:href="assets/bootstrap-icons.svg#upc-scan"/>
</svg> </svg>
<small>#{{document.archive_serial_number}}</small> <small>#{{document.archive_serial_number}}</small>
</div> </div>
}
<ng-template #dateTooltip> <ng-template #dateTooltip>
<div class="d-flex flex-column text-light"> <div class="d-flex flex-column text-light">
<span i18n>Created: {{ document.created | customDate }}</span> <span i18n>Created: {{ document.created | customDate }}</span>
@ -106,22 +126,28 @@
</svg> </svg>
<small>{{document.created_date | customDate:'mediumDate'}}</small> <small>{{document.created_date | customDate:'mediumDate'}}</small>
</div> </div>
<div *ngIf="document.owner && document.owner !== settingsService.currentUser.id" class="list-group-item bg-light text-dark p-1 border-0"> @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"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/> <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
</svg> </svg>
<small>{{document.owner | username}}</small> <small>{{document.owner | username}}</small>
</div> </div>
<div *ngIf="document.is_shared_by_requester" class="list-group-item bg-light text-dark p-1 border-0"> }
@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"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/> <use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
</svg> </svg>
<small i18n>Shared</small> <small i18n>Shared</small>
</div> </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"> }
@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> <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> <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>

View File

@ -11,45 +11,55 @@
</div> </div>
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6"> <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> @for (t of getTagsLimited$() | async; track t) {
<div *ngIf="moreTags"> <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> <span class="badge text-dark">+ {{moreTags}}</span>
</div> </div>
}
</div> </div>
</div> </div>
<a *ngIf="notesEnabled && document.notes.length" routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1"> @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"> <span class="badge rounded-pill bg-light border text-primary">
<svg class="metadata-icon ms-1 me-1" fill="currentColor"> <svg class="metadata-icon ms-1 me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
</svg> </svg>
{{document.notes.length}}</span> {{document.notes.length}}</span>
</a> </a>
}
<div class="card-body bg-light p-2"> <div class="card-body bg-light p-2">
<p class="card-text"> <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>: <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}} {{document.title | documentTitle}}
</p> </p>
</div> </div>
<div class="card-footer pt-0 pb-2 px-2"> <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"> <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 @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()"> (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark"/> <use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
</svg> </svg>
<small>{{(document.document_type$ | async)?.name ?? privateName}}</small> <small>{{(document.document_type$ | async)?.name ?? privateName}}</small>
</button> </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 }
@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()"> (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#folder"/> <use xlink:href="assets/bootstrap-icons.svg#folder"/>
</svg> </svg>
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small> <small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
</button> </button>
}
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between"> <div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
<ng-template #dateTooltip> <ng-template #dateTooltip>
<div class="d-flex flex-column text-light"> <div class="d-flex flex-column text-light">
@ -65,24 +75,30 @@
<small>{{document.created_date | customDate:'mediumDate'}}</small> <small>{{document.created_date | customDate:'mediumDate'}}</small>
</div> </div>
</div> </div>
<div *ngIf="document.archive_serial_number | isNumber" class="ps-0 p-1"> @if (document.archive_serial_number | isNumber) {
<div class="ps-0 p-1">
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#upc-scan"/> <use xlink:href="assets/bootstrap-icons.svg#upc-scan"/>
</svg> </svg>
<small>#{{document.archive_serial_number}}</small> <small>#{{document.archive_serial_number}}</small>
</div> </div>
<div *ngIf="document.owner && document.owner !== settingsService.currentUser.id" class="ps-0 p-1"> }
@if (document.owner && document.owner !== settingsService.currentUser.id) {
<div class="ps-0 p-1">
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/> <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
</svg> </svg>
<small>{{document.owner | username}}</small> <small>{{document.owner | username}}</small>
</div> </div>
<div *ngIf="document.is_shared_by_requester" class="ps-0 p-1"> }
@if (document.is_shared_by_requester) {
<div class="ps-0 p-1">
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/> <use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
</svg> </svg>
<small i18n>Shared</small> <small i18n>Shared</small>
</div> </div>
}
</div> </div>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div class="btn-group w-100"> <div class="btn-group w-100">

View File

@ -52,9 +52,11 @@
</label> </label>
</div> </div>
<div> <div>
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSortField(f.field)" @for (f of getSortFields(); track f) {
<button ngbDropdownItem (click)="setSortField(f.field)"
[class.active]="list.sortField === f.field">{{f.name}} [class.active]="list.sortField === f.field">{{f.name}}
</button> </button>
}
</div> </div>
</div> </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"> <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> <button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle>
<ng-container i18n>Views</ng-container> <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"> @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> <span class="visually-hidden">selected</span>
</div> </div>
}
</button> </button>
<div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu> <div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu>
<ng-container *ngIf="!list.activeSavedViewId"> @if (!list.activeSavedViewId) {
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view.id)">{{view.name}}</button> @for (view of savedViewService.allViews; track view) {
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div> <button ngbDropdownItem (click)="loadViewConfig(view.id)">{{view.name}}</button>
</ng-container> }
@if (savedViewService.allViews.length > 0) {
<div class="dropdown-divider"></div>
}
}
<div *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.SavedView }"> <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> </div>
<button ngbDropdownItem (click)="saveViewConfigAs()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.SavedView }" i18n>Save as...</button> <button ngbDropdownItem (click)="saveViewConfigAs()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.SavedView }" i18n>Save as...</button>
</div> </div>
@ -90,22 +100,32 @@
<ng-template #pagination> <ng-template #pagination>
<div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3"> <div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center"> <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> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container> <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> @if (list.selected.size > 0) {
<ng-container *ngIf="!list.isReloading"> <span i18n>{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span>
<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> @if (!list.isReloading) {
<button *ngIf="!list.isReloading && isFiltered" class="btn btn-link py-0" (click)="resetFilters()"> @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"> <svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg><small i18n>Reset filters</small> </svg><small i18n>Reset filters</small>
</button> </button>
}
</div> </div>
<ngb-pagination *ngIf="list.collectionSize" [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" @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> [rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination>
}
</div> </div>
</ng-template> </ng-template>
@ -113,17 +133,19 @@
<ng-container *ngTemplateOutlet="pagination"></ng-container> <ng-container *ngTemplateOutlet="pagination"></ng-container>
</div> </div>
<ng-container *ngIf="list.error ; else documentListNoError"> @if (list.error ) {
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div> <div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
</ng-container> } @else {
@if (displayMode === 'largeCards') {
<ng-template #documentListNoError> <div>
<div *ngIf="displayMode === 'largeCards'"> @for (d of list.documents; track trackByDocumentId($index, d)) {
<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 [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> </pngx-document-card-large>
}
</div> </div>
}
<table class="table table-sm align-middle border shadow-sm" *ngIf="displayMode === 'details'"> @if (displayMode === 'details') {
<table class="table table-sm align-middle border shadow-sm">
<thead> <thead>
<th></th> <th></th>
<th class="d-none d-lg-table-cell" <th class="d-none d-lg-table-cell"
@ -155,13 +177,15 @@
[currentSortReverse]="list.sortReverse" [currentSortReverse]="list.sortReverse"
(sort)="onSort($event)" (sort)="onSort($event)"
i18n>Owner</th> i18n>Owner</th>
<th *ngIf="notesEnabled" class="d-none d-xl-table-cell" @if (notesEnabled) {
<th class="d-none d-xl-table-cell"
pngxSortable="num_notes" pngxSortable="num_notes"
title="Sort by notes" i18n-title title="Sort by notes" i18n-title
[currentSortField]="list.sortField" [currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse" [currentSortReverse]="list.sortReverse"
(sort)="onSort($event)" (sort)="onSort($event)"
i18n>Notes</th> i18n>Notes</th>
}
<th class="d-none d-xl-table-cell" <th class="d-none d-xl-table-cell"
pngxSortable="document_type__name" pngxSortable="document_type__name"
title="Sort by document type" i18n-title title="Sort by document type" i18n-title
@ -192,7 +216,8 @@
i18n>Added</th> i18n>Added</th>
</thead> </thead>
<tbody> <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' : ''"> @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> <td>
<div class="form-check"> <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();"> <input type="checkbox" class="form-check-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event); $event.stopPropagation();">
@ -203,35 +228,41 @@
{{d.archive_serial_number}} {{d.archive_serial_number}}
</td> </td>
<td class="d-none d-md-table-cell"> <td class="d-none d-md-table-cell">
<ng-container *ngIf="d.correspondent"> @if (d.correspondent) {
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a> <a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
</ng-container> }
</td> </td>
<td> <td>
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> <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> @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>
<td> <td>
{{d.owner | username}} {{d.owner | username}}
</td> </td>
<td *ngIf="notesEnabled" class="d-none d-xl-table-cell"> @if (notesEnabled) {
<a *ngIf="d.notes.length" routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0"> <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"> <span class="badge rounded-pill bg-light border text-primary">
<svg class="metadata-icon ms-1 me-1" fill="currentColor"> <svg class="metadata-icon ms-1 me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
</svg> </svg>
{{d.notes.length}}</span> {{d.notes.length}}</span>
</a> </a>
}
</td> </td>
}
<td class="d-none d-xl-table-cell"> <td class="d-none d-xl-table-cell">
<ng-container *ngIf="d.document_type"> @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> <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>
<td class="d-none d-xl-table-cell"> <td class="d-none d-xl-table-cell">
<ng-container *ngIf="d.storage_path"> @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> <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>
<td> <td>
{{d.created_date | customDate}} {{d.created_date | customDate}}
@ -240,15 +271,20 @@
{{d.added | customDate}} {{d.added | customDate}}
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
}
<div class="row row-cols-paperless-cards" *ngIf="displayMode === 'smallCards'"> @if (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 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> </div>
<div *ngIf="list.documents?.length > 15" class="mt-3"> }
@if (list.documents?.length > 15) {
<div class="mt-3">
<ng-container *ngTemplateOutlet="pagination"></ng-container> <ng-container *ngTemplateOutlet="pagination"></ng-container>
</div> </div>
}
}
</ng-template>

View File

@ -5,17 +5,25 @@
<div ngbDropdown> <div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button> <button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button>
<div class="dropdown-menu shadow" ngbDropdownMenu> <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>
</div> </div>
<select *ngIf="textFilterTarget === 'asn'" class="form-select flex-grow-0 w-auto" [(ngModel)]="textFilterModifier" (change)="textFilterModifierChange()"> @if (textFilterTarget === 'asn') {
<option *ngFor="let m of textFilterModifiers" ngbDropdownItem [value]="m.id">{{m.label}}</option> <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> </select>
<button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()"> }
@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"> <svg fill="currentColor" class="buttonicon-sm me-1">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
</button> </button>
}
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget === 'fulltext-morelike'"> <input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget === 'fulltext-morelike'">
</div> </div>
</div> </div>

View File

@ -1726,4 +1726,13 @@ describe('FilterEditorComponent', () => {
) )
expect(component.textFilter).toEqual('') 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({ @Component({
selector: 'pngx-filter-editor', selector: 'pngx-filter-editor',
templateUrl: './filter-editor.component.html', templateUrl: './filter-editor.component.html',
@ -200,29 +245,12 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
_moreLikeDoc: PaperlessDocument _moreLikeDoc: PaperlessDocument
get textFilterTargets() { 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) { if (this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE) {
targets.push({ return DEFAULT_TEXT_FILTER_TARGET_OPTIONS.concat([
id: TEXT_FILTER_TARGET_FULLTEXT_MORELIKE, TEXT_FILTER_TARGET_MORELIKE_OPTION,
name: $localize`More like`, ])
})
} }
return targets return DEFAULT_TEXT_FILTER_TARGET_OPTIONS
} }
textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT
@ -235,28 +263,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
public textFilterModifier: string public textFilterModifier: string
get textFilterModifiers() { get textFilterModifiers() {
return [ return 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`,
},
]
} }
get textFilterModifierIsNull(): boolean { 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-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 in sidebar" formControlName="showInSideBar"></pngx-input-check>
<pngx-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></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"> @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> <h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6>
<ng-container i18n>The error returned was</ng-container>:<br/> <ng-container i18n>The error returned was</ng-container>:<br/>
{{ error.filter_rules }} {{ error.filter_rules }}
</div> </div>
}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="!buttonsEnabled">Cancel</button> <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="!buttonsEnabled">Cancel</button>

View File

@ -1,4 +1,5 @@
<div *ngIf="notes"> @if (notes) {
<div>
<form [formGroup]="noteForm" class="needs-validation mt-3" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Note }" novalidate> <form [formGroup]="noteForm" class="needs-validation mt-3" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Note }" novalidate>
<div class="form-group"> <div class="form-group">
<textarea class="form-control form-control-sm" [class.is-invalid]="newNoteError" rows="3" formControlName="newNote" placeholder="Enter note" i18n-placeholder (keydown)="noteFormKeydown($event)" required></textarea> <textarea class="form-control form-control-sm" [class.is-invalid]="newNoteError" rows="3" formControlName="newNote" placeholder="Enter note" i18n-placeholder (keydown)="noteFormKeydown($event)" required></textarea>
@ -7,12 +8,15 @@
</div> </div>
</div> </div>
<div class="form-group mt-2 d-flex justify-content-end align-items-center"> <div class="form-group mt-2 d-flex justify-content-end align-items-center">
<div *ngIf="networkActive" class="spinner-border spinner-border-sm fw-normal me-auto" role="status"></div> @if (networkActive) {
<div class="spinner-border spinner-border-sm fw-normal me-auto" role="status"></div>
}
<button type="button" class="btn btn-primary btn-sm" [disabled]="networkActive || addDisabled" (click)="addNote()" i18n>Add note</button> <button type="button" class="btn btn-primary btn-sm" [disabled]="networkActive || addDisabled" (click)="addNote()" i18n>Add note</button>
</div> </div>
</form> </form>
<hr> <hr>
<div *ngFor="let note of notes" class="card border mb-3"> @for (note of notes; track note) {
<div class="card border mb-3">
<div class="card-body text-dark"> <div class="card-body text-dark">
<p class="card-text">{{note.note}}</p> <p class="card-text">{{note.note}}</p>
</div> </div>
@ -26,4 +30,6 @@
</button> </button>
</div> </div>
</div> </div>
}
</div> </div>
}

View File

@ -18,7 +18,8 @@
</div> </div>
</li> </li>
<li *ngFor="let template of templates" class="list-group-item"> @for (template of templates; track template) {
<li class="list-group-item">
<div class="row"> <div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editTemplate(template)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.ConsumptionTemplate)">{{template.name}}</button></div> <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editTemplate(template)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.ConsumptionTemplate)">{{template.name}}</button></div>
<div class="col d-flex align-items-center"><code>{{template.order}}</code></div> <div class="col d-flex align-items-center"><code>{{template.order}}</code></div>
@ -39,5 +40,8 @@
</div> </div>
</div> </div>
</li> </li>
<li *ngIf="templates.length === 0" class="list-group-item" i18n>No templates defined.</li> }
@if (templates.length === 0) {
<li class="list-group-item" i18n>No templates defined.</li>
}
</ul> </ul>

View File

@ -17,7 +17,8 @@
</div> </div>
</li> </li>
<li *ngFor="let field of fields" class="list-group-item"> @for (field of fields; track field) {
<li class="list-group-item">
<div class="row"> <div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div> <div class="col d-flex align-items-center"><button class="btn btn-link p-0" 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 d-flex align-items-center">{{getDataType(field)}}</div>
@ -37,5 +38,8 @@
</div> </div>
</div> </div>
</li> </li>
<li *ngIf="fields.length === 0" class="list-group-item" i18n>No fields defined.</li> }
@if (fields.length === 0) {
<li class="list-group-item" i18n>No fields defined.</li>
}
</ul> </ul>

View File

@ -20,7 +20,8 @@
</div> </div>
</li> </li>
<li *ngFor="let account of mailAccounts" class="list-group-item"> @for (account of mailAccounts; track account) {
<li class="list-group-item">
<div class="row"> <div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">{{account.name}}</button></div> <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">{{account.name}}</button></div>
<div class="col d-flex align-items-center">{{account.imap_server}}</div> <div class="col d-flex align-items-center">{{account.imap_server}}</div>
@ -45,7 +46,10 @@
</div> </div>
</div> </div>
</li> </li>
<li *ngIf="mailAccounts.length === 0" class="list-group-item" i18n>No mail accounts defined.</li> }
@if (mailAccounts.length === 0) {
<li class="list-group-item" i18n>No mail accounts defined.</li>
}
</ul> </ul>
</ng-container> </ng-container>
@ -69,7 +73,8 @@
</div> </div>
</li> </li>
<li *ngFor="let rule of mailRules" class="list-group-item"> @for (rule of mailRules; track rule) {
<li class="list-group-item">
<div class="row"> <div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div> <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div>
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div> <div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
@ -94,12 +99,17 @@
</div> </div>
</div> </div>
</li> </li>
<li *ngIf="mailRules.length === 0" class="list-group-item" i18n>No mail rules defined.</li> }
@if (mailRules.length === 0) {
<li class="list-group-item" i18n>No mail rules defined.</li>
}
</ul> </ul>
</ng-container> </ng-container>
<div *ngIf="!mailAccounts || !mailRules"> @if (!mailAccounts || !mailRules) {
<div>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div> <div class="visually-hidden" i18n>Loading...</div>
</div> </div>
}

View File

@ -41,18 +41,23 @@
<th scope="col" class="fw-normal" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th> <th scope="col" class="fw-normal" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" class="fw-normal d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th> <th scope="col" class="fw-normal d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" class="fw-normal" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th> <th scope="col" class="fw-normal" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
<th scope="col" class="fw-normal" *ngFor="let column of extraColumns" pngxSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th> @for (column of extraColumns; track column) {
<th scope="col" class="fw-normal" pngxSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
}
<th scope="col" class="fw-normal" i18n>Actions</th> <th scope="col" class="fw-normal" i18n>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngIf="isLoading"> @if (isLoading) {
<tr>
<td colspan="5"> <td colspan="5">
<div class="spinner-border spinner-border-sm me-2" role="status"></div> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container> <ng-container i18n>Loading...</ng-container>
</td> </td>
</tr> </tr>
<tr *ngFor="let object of data" (click)="toggleSelected(object, $event); $event.stopPropagation();"> }
@for (object of data; track object) {
<tr (click)="toggleSelected(object); $event.stopPropagation();">
<td> <td>
<div class="form-check m-0 ms-2 me-n2"> <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();"> <input type="checkbox" class="form-check-input" id="{{typeName}}{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();">
@ -62,10 +67,15 @@
<td scope="row"><button class="btn btn-link ms-0 ps-0 text-start" (click)="openEditDialog(object)">{{ object.name }}</button> </td> <td scope="row"><button class="btn btn-link ms-0 ps-0 text-start" (click)="openEditDialog(object)">{{ object.name }}</button> </td>
<td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td> <td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
<td scope="row">{{ object.document_count }}</td> <td scope="row">{{ object.document_count }}</td>
<td scope="row" *ngFor="let column of extraColumns"> @for (column of extraColumns; track column) {
<div *ngIf="column.rendersHtml; else colValue" [innerHtml]="column.valueFn.call(null, object) | safeHtml"></div> <td scope="row">
<ng-template #colValue>{{ column.valueFn.call(null, object) }}</ng-template> @if (column.rendersHtml) {
<div [innerHtml]="column.valueFn.call(null, object) | safeHtml"></div>
} @else {
{{ column.valueFn.call(null, object) }}
}
</td> </td>
}
<td scope="row"> <td scope="row">
<div class="btn-group d-block d-sm-none"> <div class="btn-group d-block d-sm-none">
<div ngbDropdown class="d-inline-block"> <div ngbDropdown class="d-inline-block">
@ -100,14 +110,23 @@
</div> </div>
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="d-flex mb-2" *ngIf="!isLoading"> @if (!isLoading) {
<div *ngIf="collectionSize > 0"> <div class="d-flex mb-2">
@if (collectionSize > 0) {
<div>
<ng-container i18n>{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</ng-container> <ng-container i18n>{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</ng-container>
<ng-container *ngIf="selectedObjects.size > 0">&nbsp;({{selectedObjects.size}} selected)</ng-container> @if (selectedObjects.size > 0) {
&nbsp;({{selectedObjects.size}} selected)
}
</div> </div>
<ngb-pagination *ngIf="collectionSize > 20" class="ms-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination> }
@if (collectionSize > 20) {
<ngb-pagination class="ms-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
}
</div> </div>
}

View File

@ -38,120 +38,7 @@ export interface LanguageOption {
dateInputFormat?: string dateInputFormat?: string
} }
@Injectable({ const LANGUAGE_OPTIONS = [
providedIn: 'root',
})
export class SettingsService {
protected baseUrl: string = environment.apiBaseUrl + 'ui_settings/'
private settings: Object = {}
currentUser: PaperlessUser
public settingsSaved: EventEmitter<any> = new EventEmitter()
private _renderer: Renderer2
public get renderer(): Renderer2 {
return this._renderer
}
public dashboardIsEmpty: boolean = false
public globalDropzoneEnabled: boolean = true
public globalDropzoneActive: boolean = false
public organizingSidebarSavedViews: boolean = false
constructor(
rendererFactory: RendererFactory2,
@Inject(DOCUMENT) private document,
private cookieService: CookieService,
private meta: Meta,
@Inject(LOCALE_ID) private localeId: string,
protected http: HttpClient,
private toastService: ToastService,
private permissionsService: PermissionsService
) {
this._renderer = rendererFactory.createRenderer(null, null)
}
// this is called by the app initializer in app.module
public initializeSettings(): Observable<PaperlessUiSettings> {
return this.http.get<PaperlessUiSettings>(this.baseUrl).pipe(
first(),
tap((uisettings) => {
Object.assign(this.settings, uisettings.settings)
this.maybeMigrateSettings()
// to update lang cookie
if (this.settings['language']?.length)
this.setLanguage(this.settings['language'])
this.currentUser = uisettings.user
this.permissionsService.initialize(
uisettings.permissions,
this.currentUser
)
})
)
}
get displayName(): string {
return (
this.currentUser.first_name ??
this.currentUser.username ??
''
).trim()
}
public updateAppearanceSettings(
darkModeUseSystem = null,
darkModeEnabled = null,
themeColor = null
): void {
darkModeUseSystem ??= this.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)
darkModeEnabled ??= this.get(SETTINGS_KEYS.DARK_MODE_ENABLED)
themeColor ??= this.get(SETTINGS_KEYS.THEME_COLOR)
if (darkModeUseSystem) {
this._renderer.setAttribute(
this.document.documentElement,
'data-bs-theme',
'auto'
)
} else {
this._renderer.setAttribute(
this.document.documentElement,
'data-bs-theme',
darkModeEnabled ? 'dark' : 'light'
)
}
if (themeColor) {
const hsl = hexToHsl(themeColor)
const bgBrightnessEstimate = estimateBrightnessForColor(themeColor)
if (bgBrightnessEstimate == BRIGHTNESS.DARK) {
this._renderer.addClass(this.document.body, 'primary-dark')
this._renderer.removeClass(this.document.body, 'primary-light')
} else {
this._renderer.addClass(this.document.body, 'primary-light')
this._renderer.removeClass(this.document.body, 'primary-dark')
}
document.documentElement.style.setProperty(
'--pngx-primary',
`${+hsl.h * 360},${hsl.s * 100}%`
)
document.documentElement.style.setProperty(
'--pngx-primary-lightness',
`${hsl.l * 100}%`
)
} else {
this._renderer.removeClass(this.document.body, 'primary-dark')
this._renderer.removeClass(this.document.body, 'primary-light')
document.documentElement.style.removeProperty('--pngx-primary')
document.documentElement.style.removeProperty('--pngx-primary-lightness')
}
}
getLanguageOptions(): LanguageOption[] {
const languages = [
{ {
code: 'en-us', code: 'en-us',
name: $localize`English (US)`, name: $localize`English (US)`,
@ -340,21 +227,133 @@ export class SettingsService {
}, },
] ]
// Sort languages by localized name at runtime const ISO_LANGUAGE_OPTION: LanguageOption = {
languages.sort((a, b) => {
return a.name < b.name ? -1 : 1
})
return languages
}
getDateLocaleOptions(): LanguageOption[] {
let isoOption: LanguageOption = {
code: 'iso-8601', code: 'iso-8601',
name: $localize`ISO 8601`, name: $localize`ISO 8601`,
dateInputFormat: 'yyyy-mm-dd', dateInputFormat: 'yyyy-mm-dd',
} }
return [isoOption].concat(this.getLanguageOptions())
@Injectable({
providedIn: 'root',
})
export class SettingsService {
protected baseUrl: string = environment.apiBaseUrl + 'ui_settings/'
private settings: Object = {}
currentUser: PaperlessUser
public settingsSaved: EventEmitter<any> = new EventEmitter()
private _renderer: Renderer2
public get renderer(): Renderer2 {
return this._renderer
}
public dashboardIsEmpty: boolean = false
public globalDropzoneEnabled: boolean = true
public globalDropzoneActive: boolean = false
public organizingSidebarSavedViews: boolean = false
constructor(
rendererFactory: RendererFactory2,
@Inject(DOCUMENT) private document,
private cookieService: CookieService,
private meta: Meta,
@Inject(LOCALE_ID) private localeId: string,
protected http: HttpClient,
private toastService: ToastService,
private permissionsService: PermissionsService
) {
this._renderer = rendererFactory.createRenderer(null, null)
}
// this is called by the app initializer in app.module
public initializeSettings(): Observable<PaperlessUiSettings> {
return this.http.get<PaperlessUiSettings>(this.baseUrl).pipe(
first(),
tap((uisettings) => {
Object.assign(this.settings, uisettings.settings)
this.maybeMigrateSettings()
// to update lang cookie
if (this.settings['language']?.length)
this.setLanguage(this.settings['language'])
this.currentUser = uisettings.user
this.permissionsService.initialize(
uisettings.permissions,
this.currentUser
)
})
)
}
get displayName(): string {
return (
this.currentUser.first_name ??
this.currentUser.username ??
''
).trim()
}
public updateAppearanceSettings(
darkModeUseSystem = null,
darkModeEnabled = null,
themeColor = null
): void {
darkModeUseSystem ??= this.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)
darkModeEnabled ??= this.get(SETTINGS_KEYS.DARK_MODE_ENABLED)
themeColor ??= this.get(SETTINGS_KEYS.THEME_COLOR)
if (darkModeUseSystem) {
this._renderer.setAttribute(
this.document.documentElement,
'data-bs-theme',
'auto'
)
} else {
this._renderer.setAttribute(
this.document.documentElement,
'data-bs-theme',
darkModeEnabled ? 'dark' : 'light'
)
}
if (themeColor) {
const hsl = hexToHsl(themeColor)
const bgBrightnessEstimate = estimateBrightnessForColor(themeColor)
if (bgBrightnessEstimate == BRIGHTNESS.DARK) {
this._renderer.addClass(this.document.body, 'primary-dark')
this._renderer.removeClass(this.document.body, 'primary-light')
} else {
this._renderer.addClass(this.document.body, 'primary-light')
this._renderer.removeClass(this.document.body, 'primary-dark')
}
document.documentElement.style.setProperty(
'--pngx-primary',
`${+hsl.h * 360},${hsl.s * 100}%`
)
document.documentElement.style.setProperty(
'--pngx-primary-lightness',
`${hsl.l * 100}%`
)
} else {
this._renderer.removeClass(this.document.body, 'primary-dark')
this._renderer.removeClass(this.document.body, 'primary-light')
document.documentElement.style.removeProperty('--pngx-primary')
document.documentElement.style.removeProperty('--pngx-primary-lightness')
}
}
getLanguageOptions(): LanguageOption[] {
// Sort languages by localized name at runtime
return LANGUAGE_OPTIONS.sort((a, b) => {
return a.name < b.name ? -1 : 1
})
}
getDateLocaleOptions(): LanguageOption[] {
return [ISO_LANGUAGE_OPTION].concat(this.getLanguageOptions())
} }
private getLanguageCookieName() { private getLanguageCookieName() {