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

@ -1,32 +1,36 @@
<pngx-toasts></pngx-toasts> <pngx-toasts></pngx-toasts>
<pngx-file-drop> <pngx-file-drop>
<ng-container content> <ng-container content>
<router-outlet></router-outlet> <router-outlet></router-outlet>
</ng-container> </ng-container>
</pngx-file-drop> </pngx-file-drop>
<tour-step-template> <tour-step-template>
<ng-template #tourStep let-step="step"> <ng-template #tourStep let-step="step">
<p class="tour-step-content" [innerHTML]="step?.content"></p> <p class="tour-step-content" [innerHTML]="step?.content"></p>
<hr/> <hr/>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<span class="badge bg-light text-dark">{{ tourService.steps?.indexOf(step) + 1 }} / {{ tourService.steps?.length }}</span> <span class="badge bg-light text-dark">{{ tourService.steps?.indexOf(step) + 1 }} / {{ tourService.steps?.length }}</span>
<div class="tour-step-navigation btn-toolbar" role="toolbar" aria-label="Controls"> <div class="tour-step-navigation btn-toolbar" role="toolbar" aria-label="Controls">
<div class="btn-group btn-group-sm me-2" role="group" aria-label="Dismiss"> <div class="btn-group btn-group-sm me-2" role="group" aria-label="Dismiss">
<button class="btn btn-outline-danger" (click)="tourService.end()"> <button class="btn btn-outline-danger" (click)="tourService.end()">
{{ step?.endBtnTitle }} {{ step?.endBtnTitle }}
</button> </button>
</div>
<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()">
« {{ step?.prevBtnTitle }}
</button>
<button *ngIf="tourService.hasNext(step)" class="btn btn-outline-primary" (click)="tourService.next()">
{{ step?.nextBtnTitle }} »
</button>
</div>
</div>
</div> </div>
</ng-template> <div class="btn-group btn-group-sm align-self-end" role="group" aria-label="Previous / Next">
@if (tourService.hasPrev(step)) {
<button class="btn btn-outline-primary" (click)="tourService.prev()">
« {{ step?.prevBtnTitle }}
</button>
}
@if (tourService.hasNext(step)) {
<button class="btn btn-outline-primary" (click)="tourService.next()">
{{ step?.nextBtnTitle }} »
</button>
}
</div>
</div>
</div>
</ng-template>
</tour-step-template> </tour-step-template>

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) {
<a ngbNavLink> <li [ngbNavItem]="logFile">
{{logFile}}.log <a ngbNavLink>
</a> {{logFile}}.log
</li> </a>
<div *ngIf="isLoading || !logFiles.length" class="ps-2 d-flex align-items-center"> </li>
<div class="spinner-border spinner-border-sm me-2" role="status"></div> }
<ng-container *ngIf="!logFiles.length" i18n>Loading...</ng-container> @if (isLoading || !logFiles.length) {
</div> <div class="ps-2 d-flex align-items-center">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
@if (!logFiles.length) {
<ng-container i18n>Loading...</ng-container>
}
</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 class="spinner-border spinner-border-sm me-2" role="status"></div> <div>
<ng-container i18n>Loading...</ng-container> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
</div> <ng-container i18n>Loading...</ng-container>
<p </div>
class="m-0 p-0 log-entry-{{getLogLevel(log)}}" }
*ngFor="let log of logs">{{log}}</p> @for (log of logs; track log) {
<p
class="m-0 p-0 log-entry-{{getLogLevel(log)}}"
>{{log}}</p>
}
</div> </div>

View File

@ -1,10 +1,10 @@
<pngx-page-header title="Settings" i18n-title> <pngx-page-header title="Settings" i18n-title>
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button> <button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank"> <a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
<ng-container i18n>Open Django Admin</ng-container> <ng-container i18n>Open Django Admin</ng-container>
<svg class="sidebaricon ms-1" fill="currentColor"> <svg class="sidebaricon ms-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/> <use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/>
</svg> </svg>
</a> </a>
</pngx-page-header> </pngx-page-header>
@ -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>
@ -127,198 +137,202 @@
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()"> <button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
<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><ng-container i18n>Reset</ng-container> </svg><ng-container i18n>Reset</ng-container>
</button> </button>
</div>
</div> </div>
</div>
<h4 class="mt-4" id="update-checking" i18n>Update checking</h4> <h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
<div class="row mb-3"> <div class="row mb-3">
<div class="offset-md-3 col"> <div class="offset-md-3 col">
<p i18n> <p i18n>
Update checking works by pinging the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">GitHub API</a> for the latest release to determine whether a new version is available.<br/> Update checking works by pinging the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">GitHub API</a> for the latest release to determine whether a new version is available.<br/>
Actual updating of the app must still be performed manually. Actual updating of the app must still be performed manually.
</p> </p>
<p i18n> <p i18n>
<em>No tracking data is collected by the app in any way.</em> <em>No tracking data is collected by the app in any way.</em>
</p> </p>
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check> <pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
</div>
</div> </div>
</div>
<h4 class="mt-4" i18n>Bulk editing</h4> <h4 class="mt-4" i18n>Bulk editing</h4>
<div class="row mb-3"> <div class="row mb-3">
<div class="offset-md-3 col"> <div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></pngx-input-check> <pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></pngx-input-check>
<pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check> <pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
</div>
</div> </div>
</div>
<h4 class="mt-4" i18n>Notes</h4> <h4 class="mt-4" i18n>Notes</h4>
<div class="row mb-3"> <div class="row mb-3">
<div class="offset-md-3 col"> <div class="offset-md-3 col">
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check> <pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
</div>
</div> </div>
</div>
</ng-template> </ng-template>
</li> </li>
<li [ngbNavItem]="SettingsNavIDs.Permissions"> <li [ngbNavItem]="SettingsNavIDs.Permissions">
<a ngbNavLink i18n>Permissions</a> <a ngbNavLink i18n>Permissions</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<h4 i18n>Default Permissions</h4> <h4 i18n>Default Permissions</h4>
<div class="row mb-3"> <div class="row mb-3">
<div class="offset-md-3 col"> <div class="offset-md-3 col">
<p i18n> <p i18n>
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
</p> </p>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Default Owner</span>
</div>
<div class="col-md-5">
<pngx-input-select [items]="users" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Default View Permissions</span>
</div>
<div class="col-md-5">
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Users:</span>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<pngx-permissions-user type="view" formControlName="defaultPermsViewUsers"></pngx-permissions-user>
</ng-container>
</div>
</div>
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Groups:</span>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
<pngx-permissions-group type="view" formControlName="defaultPermsViewGroups"></pngx-permissions-group>
</ng-container>
</div>
</div> </div>
</div> </div>
</div> <div class="row mb-3">
<div class="row mb-3"> <div class="col-md-3 col-form-label pt-0">
<div class="col-md-3 col-form-label pt-0"> <span i18n>Default Owner</span>
<span i18n>Default Edit Permissions</span>
</div>
<div class="col-md-5">
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Users:</span>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<pngx-permissions-user type="view" formControlName="defaultPermsEditUsers"></pngx-permissions-user>
</ng-container>
</div>
</div> </div>
<div class="row"> <div class="col-md-5">
<div class="col-3"> <pngx-input-select [items]="users" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
<span class="d-block pt-1" i18n>Groups:</span> <small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
<pngx-permissions-group type="view" formControlName="defaultPermsEditGroups"></pngx-permissions-group>
</ng-container>
</div>
</div>
<div class="row">
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
</div> </div>
</div> </div>
</div> <div class="row mb-3">
</ng-template> <div class="col-md-3 col-form-label pt-0">
</li> <span i18n>Default View Permissions</span>
</div>
<li [ngbNavItem]="SettingsNavIDs.Notifications"> <div class="col-md-5">
<a ngbNavLink i18n>Notifications</a> <div class="row">
<ng-template ngbNavContent> <div class="col-3">
<span class="d-block pt-1" i18n>Users:</span>
<h4 i18n>Document processing</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show notifications when new documents are detected" formControlName="notificationsConsumerNewDocument"></pngx-input-check>
<pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check>
<pngx-input-check i18n-title title="Show notifications when document processing fails" formControlName="notificationsConsumerFailed"></pngx-input-check>
<pngx-input-check i18n-title title="Suppress notifications on dashboard" formControlName="notificationsConsumerSuppressOnDashboard" i18n-hint hint="This will suppress all messages about document processing status on the dashboard."></pngx-input-check>
</div>
</div>
</ng-template>
</li>
<li [ngbNavItem]="SettingsNavIDs.SavedViews">
<a ngbNavLink i18n>Saved views</a>
<ng-template ngbNavContent>
<h4 i18n>Settings</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
</div>
</div>
<h4 i18n>Views</h4>
<div formGroupName="savedViews">
<div *ngFor="let view of savedViews" [formGroupName]="view.id" class="row">
<div class="mb-3 col">
<label class="form-label" for="name_{{view.id}}" i18n>Name</label>
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
</div>
<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>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
</div> </div>
<div class="form-check form-switch"> <div class="col">
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar"> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label> <pngx-permissions-user type="view" formControlName="defaultPermsViewUsers"></pngx-permissions-user>
</ng-container>
</div> </div>
</div> </div>
<div class="row">
<div class="mb-2 col-auto"> <div class="col-3">
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label> <span class="d-block pt-1" i18n>Groups:</span>
<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 class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
<pngx-permissions-group type="view" formControlName="defaultPermsViewGroups"></pngx-permissions-group>
</ng-container>
</div>
</div> </div>
</div> </div>
</div>
<div *ngIf="savedViews && savedViews.length === 0" i18n>No saved views defined.</div> <div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<div *ngIf="!savedViews"> <span i18n>Default Edit Permissions</span>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</div> </div>
<div class="col-md-5">
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Users:</span>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<pngx-permissions-user type="view" formControlName="defaultPermsEditUsers"></pngx-permissions-user>
</ng-container>
</div>
</div>
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Groups:</span>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
<pngx-permissions-group type="view" formControlName="defaultPermsEditGroups"></pngx-permissions-group>
</ng-container>
</div>
</div>
<div class="row">
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
</div>
</div>
</div>
</ng-template>
</li>
</div> <li [ngbNavItem]="SettingsNavIDs.Notifications">
<a ngbNavLink i18n>Notifications</a>
<ng-template ngbNavContent>
</ng-template> <h4 i18n>Document processing</h4>
</li>
</ul>
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div> <div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show notifications when new documents are detected" formControlName="notificationsConsumerNewDocument"></pngx-input-check>
<pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check>
<pngx-input-check i18n-title title="Show notifications when document processing fails" formControlName="notificationsConsumerFailed"></pngx-input-check>
<pngx-input-check i18n-title title="Suppress notifications on dashboard" formControlName="notificationsConsumerSuppressOnDashboard" i18n-hint hint="This will suppress all messages about document processing status on the dashboard."></pngx-input-check>
</div>
</div>
<button type="submit" class="btn btn-primary mb-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" [disabled]="(isDirty$ | async) === false" i18n>Save</button> </ng-template>
</form> </li>
<li [ngbNavItem]="SettingsNavIDs.SavedViews">
<a ngbNavLink i18n>Saved views</a>
<ng-template ngbNavContent>
<h4 i18n>Settings</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
</div>
</div>
<h4 i18n>Views</h4>
<div formGroupName="savedViews">
@for (view of savedViews; track view) {
<div [formGroupName]="view.id" class="row">
<div class="mb-3 col">
<label class="form-label" for="name_{{view.id}}" i18n>Name</label>
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
</div>
<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>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
</div>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
</div>
</div>
<div class="mb-2 col-auto">
<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>
</div>
</div>
}
@if (savedViews && savedViews.length === 0) {
<div i18n>No saved views defined.</div>
}
@if (!savedViews) {
<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>
}
</div>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
<button type="submit" class="btn btn-primary mb-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
</form>

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

@ -3,127 +3,153 @@
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0"> <button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
<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>Clear selection</ng-container> </svg>&nbsp;<ng-container i18n>Clear selection</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0"> <button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#check2-all"/> <use xlink:href="assets/bootstrap-icons.svg#check2-all"/>
</svg>&nbsp;<ng-container i18n>{{dismissButtonText}}</ng-container> </svg>&nbsp;<ng-container i18n>{{dismissButtonText}}</ng-container>
</button> </button>
<div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()"> <div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval"> <input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label> <label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div> </div>
</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">
<thead> <thead>
<tr> <tr>
<th scope="col"> <th scope="col">
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" (click)="toggleAll($event); $event.stopPropagation();"> <input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
<label class="form-check-label" for="all-tasks"></label> <label class="form-check-label" for="all-tasks"></label>
</div> </div>
</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-table-cell d-lg-none" i18n>Info</th> <th scope="col" class="d-none d-lg-table-cell" i18n>Results</th>
<th scope="col" i18n>Actions</th> }
</tr> <th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
</thead> <th scope="col" i18n>Actions</th>
<tbody> </tr>
<ng-container *ngFor="let task of tasks | slice: (page-1) * pageSize : page * pageSize"> </thead>
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();"> <tbody>
<td> @for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) {
<div class="form-check"> <tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();"> <td>
<label class="form-check-label" for="task{{task.id}}"></label> <div class="form-check">
</div> <input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();">
</td> <label class="form-check-label" for="task{{task.id}}"></label>
<td class="overflow-auto name-col">{{ task.task_file_name }}</td> </div>
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td> </td>
<td class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'"> <td class="overflow-auto name-col">{{ task.task_file_name }}</td>
<div *ngIf="task.result?.length > 50" class="result" (click)="expandTask(task); $event.stopPropagation();" <td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body"> @if (activeTab !== 'started' && activeTab !== 'queued') {
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}&hellip;</span> <td class="d-none d-lg-table-cell">
</div> @if (task.result?.length > 50) {
<span *ngIf="task.result?.length <= 50" class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span> <div class="result" (click)="expandTask(task); $event.stopPropagation();"
<ng-template #resultPopover> [ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
<pre class="small mb-0">{{ task.result | slice:0:300 }}<ng-container *ngIf="task.result.length > 300">&hellip;</ng-container></pre> <span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}&hellip;</span>
<ng-container *ngIf="task.result?.length > 300"><br/><em>(<ng-container i18n>click for full output</ng-container>)</em></ng-container> </div>
</ng-template> }
</td> @if (task.result?.length <= 50) {
<td class="d-lg-none"> <span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span>
<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"> <ng-template #resultPopover>
<use xlink:href="assets/bootstrap-icons.svg#info-circle" /> <pre class="small mb-0">{{ task.result | slice:0:300 }}@if (task.result.length > 300) {
</svg> &hellip;
</button> }</pre>
</td> @if (task.result?.length > 300) {
<td scope="row"> <br/><em>(<ng-container i18n>click for full output</ng-container>)</em>
<div class="btn-group" role="group"> }
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }"> </ng-template>
<svg class="sidebaricon" fill="currentColor"> </td>
<use xlink:href="assets/bootstrap-icons.svg#check"/> }
</svg>&nbsp;<ng-container i18n>Dismiss</ng-container> <td class="d-lg-none">
</button> <button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <svg fill="currentColor" class="" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
<button *ngIf="task.related_document" class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();"> <use xlink:href="assets/bootstrap-icons.svg#info-circle" />
<svg class="sidebaricon" fill="currentColor"> </svg>
<use xlink:href="assets/bootstrap-icons.svg#file-text"/> </button>
</svg>&nbsp;<ng-container i18n>Open Document</ng-container> </td>
</button> <td scope="row">
</ng-container> <div class="btn-group" role="group">
</div> <button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
</td> <svg class="sidebaricon" fill="currentColor">
</tr> <use xlink:href="assets/bootstrap-icons.svg#check"/>
<tr> </svg>&nbsp;<ng-container i18n>Dismiss</ng-container>
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5"> </button>
<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> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
</td> @if (task.related_document) {
</tr> <button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
</ng-container> <svg class="sidebaricon" fill="currentColor">
</tbody> <use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</table> </svg>&nbsp;<ng-container i18n>Open Document</ng-container>
</button>
}
</ng-container>
</div>
</td>
</tr>
<tr>
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5">
<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>
</tr>
}
</tbody>
</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>
</div> }
</ng-template> @if (tasks.length > pageSize) {
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
}
</div>
</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) {
<ng-template ngbNavContent> <span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container> }</a>
</ng-template> <ng-template ngbNavContent>
</li> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
<li ngbNavItem="completed"> </ng-template>
<a ngbNavLink i18n>Complete<span *ngIf="tasksService.completedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span></a> </li>
<ng-template ngbNavContent> <li ngbNavItem="completed">
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container> <a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
</ng-template> <span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
</li> }</a>
<li ngbNavItem="started"> <ng-template ngbNavContent>
<a ngbNavLink i18n>Started<span *ngIf="tasksService.startedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span></a> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
<ng-template ngbNavContent> </ng-template>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container> </li>
</ng-template> <li ngbNavItem="started">
</li> <a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
<li ngbNavItem="queued"> <span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
<a ngbNavLink i18n>Queued<span *ngIf="tasksService.queuedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span></a> }</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
</ng-template> </ng-template>
</li> </li>
</ul> <li ngbNavItem="queued">
<div [ngbNavOutlet]="nav"></div> <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-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav"></div>

View File

@ -1,97 +1,104 @@
<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 }">
<svg class="sidebaricon me-1" fill="currentColor"> <svg class="sidebaricon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> <use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg> </svg>
<ng-container i18n>Add User</ng-container> <ng-container i18n>Add User</ng-container>
</button> </button>
</h4> </h4>
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item"> <li class="list-group-item">
<div class="row">
<div class="col" i18n>Username</div>
<div class="col" i18n>Name</div>
<div class="col" i18n>Groups</div>
<div class="col" i18n>Actions</div>
</div>
</li>
@for (user of users; track user) {
<li class="list-group-item">
<div class="row"> <div class="row">
<div class="col" i18n>Username</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" i18n>Name</div> <div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
<div class="col" i18n>Groups</div> <div class="col d-flex align-items-center">{{user.groups?.map(getGroupName, this).join(', ')}}</div>
<div class="col" i18n>Actions</div> <div class="col">
</div>
</li>
<li *ngFor="let user of users" class="list-group-item">
<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">{{user.first_name}} {{user.last_name}}</div>
<div class="col d-flex align-items-center">{{user.groups?.map(getGroupName, this).join(', ')}}</div>
<div class="col">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }"> <button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#pencil" /> <use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container> </svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }"> <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container> </svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button> </button>
</div>
</div>
</div> </div>
</div> </li>
</div> }
</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 }">
<svg class="sidebaricon me-1" fill="currentColor"> <svg class="sidebaricon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> <use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg> </svg>
<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) {
<li class="list-group-item"> <ul class="list-group">
<div class="row"> <li class="list-group-item">
<div class="col" i18n>Name</div> <div class="row">
<div class="col"></div> <div class="col" i18n>Name</div>
<div class="col"></div> <div class="col"></div>
<div class="col" i18n>Actions</div> <div class="col"></div>
</div> <div class="col" i18n>Actions</div>
</li> </div>
</li>
<li *ngFor="let group of groups" class="list-group-item"> @for (group of groups; track group) {
<div class="row"> <li class="list-group-item">
<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="row">
<div class="col"></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>
<div class="col"> <div class="col"></div>
<div class="btn-group"> <div class="col">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }"> <div class="btn-group">
<svg class="buttonicon-sm" fill="currentColor"> <button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#pencil" /> <use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container> </svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }"> <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container> </svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</li> </li>
<li *ngIf="groups.length === 0" class="list-group-item" i18n>No groups defined</li> }
</ul> @if (groups.length === 0) {
<li class="list-group-item" i18n>No groups defined</li>
}
</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) {
<svg fill="currentColor" class="buttonicon-sm me-1"> <button type="button" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <svg fill="currentColor" class="buttonicon-sm me-1">
</svg> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</button> </svg>
</button>
}
</form> </form>
</div> </div>
<ul ngbNav class="order-sm-3"> <ul ngbNav class="order-sm-3">
@ -42,255 +44,282 @@
<button ngbDropdownItem class="nav-link" (click)="editProfile()"> <button ngbDropdownItem class="nav-link" (click)="editProfile()">
<svg class="sidebaricon me-2" fill="currentColor"> <svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person"/> <use xlink:href="assets/bootstrap-icons.svg#person"/>
</svg><ng-container i18n>My Profile</ng-container> </svg><ng-container i18n>My Profile</ng-container>
</button> </button>
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }"> <a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
<svg class="sidebaricon me-2" fill="currentColor"> <svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear"/> <use xlink:href="assets/bootstrap-icons.svg#gear"/>
</svg><ng-container i18n>Settings</ng-container> </svg><ng-container i18n>Settings</ng-container>
</a>
<a ngbDropdownItem class="nav-link" href="accounts/logout/" (click)="onLogout()">
<svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#door-open"/>
</svg><ng-container i18n>Logout</ng-container>
</a>
<div class="dropdown-divider"></div>
<a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com">
<svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#question-circle"/>
</svg><ng-container i18n>Documentation</ng-container>
</a>
</div>
</li>
</ul>
</nav>
<div class="container-fluid">
<div class="row">
<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()">
<svg class="sidebaricon-sm" fill="currentColor">
<use *ngIf="slimSidebarEnabled" xlink:href="assets/bootstrap-icons.svg#chevron-double-right"/>
<use *ngIf="!slimSidebarEnabled" xlink:href="assets/bootstrap-icons.svg#chevron-double-left"/>
</svg>
</button>
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#house"/>
</svg><span>&nbsp;<ng-container i18n>Dashboard</ng-container></span>
</a> </a>
</li> <a ngbDropdownItem class="nav-link" href="accounts/logout/" (click)="onLogout()">
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <svg class="sidebaricon me-2" fill="currentColor">
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> <use xlink:href="assets/bootstrap-icons.svg#door-open"/>
<svg class="sidebaricon" fill="currentColor"> </svg><ng-container i18n>Logout</ng-container>
<use xlink:href="assets/bootstrap-icons.svg#files"/>
</svg><span>&nbsp;<ng-container i18n>Documents</ng-container></span>
</a>
</li>
</ul>
<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'>
<span i18n>Saved views</span>
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
</h6>
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews"
cdkDrag
[cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
cdkDragPreviewContainer="parent"
cdkDragPreviewClass="navItemDrag"
(cdkDragStarted)="onDragStart($event)"
(cdkDragEnded)="onDragEnd($event)">
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
</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> <div class="dropdown-divider"></div>
<svg class="sidebaricon text-muted" fill="currentColor"> <a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com">
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/> <svg class="sidebaricon me-2" fill="currentColor">
</svg> <use xlink:href="assets/bootstrap-icons.svg#question-circle"/>
</div> </svg><ng-container i18n>Documentation</ng-container>
</li>
</ul>
</ng-container>
<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'>
<span i18n>Open documents</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
<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">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg><span>&nbsp;{{d.title | documentTitle}}</span>
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
<svg fill="currentColor" class="toolbaricon">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
</span>
</a>
</li>
<li class="nav-item w-100" *ngIf="openDocuments.length >= 1">
<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">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg><span>&nbsp;<ng-container i18n>Close all</ng-container></span>
</a>
</li>
</ul>
</ng-container>
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted">
<span i18n>Manage</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person"/>
</svg><span>&nbsp;<ng-container i18n>Correspondents</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }" tourAnchor="tour.tags">
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
</svg><span>&nbsp;<ng-container i18n>Tags</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
</svg><span>&nbsp;<ng-container i18n>Document Types</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
</svg><span>&nbsp;<ng-container i18n>Storage Paths</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#ui-radios"/>
</svg><span>&nbsp;<ng-container i18n>Custom Fields</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.ConsumptionTemplate }" tourAnchor="tour.consumption-templates">
<a class="nav-link" routerLink="templates" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Consumption templates" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-ruled"/>
</svg><span>&nbsp;<ng-container i18n>Templates</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }" tourAnchor="tour.mail">
<a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#envelope"/>
</svg><span>&nbsp;<ng-container i18n>Mail</ng-container></span>
</a>
</li>
</ul>
<h6 class="sidebar-heading px-3 mt-auto pt-4 mb-1 text-muted">
<span i18n>Administration</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }" tourAnchor="tour.settings">
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
</svg><span>&nbsp;<ng-container i18n>Settings</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people"/>
</svg><span>&nbsp;<ng-container i18n>Users & Groups</ng-container></span>
</a>
</li>
<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">
<span *ngIf="tasksService.failedFileTasks.length > 0 && slimSidebarEnabled" class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
<svg class="sidebaricon" fill="currentColor">
<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>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
</svg><span>&nbsp;<ng-container i18n>Logs</ng-container></span>
</a>
</li>
<li class="nav-item mt-2" tourAnchor="tour.outro">
<a class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#question-circle"/>
</svg><span class="ms-1">&nbsp;<ng-container i18n>Documentation</ng-container></span>
</a>
</li>
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
<div class="px-3 py-0 text-muted small d-flex align-items-center flex-wrap">
<div class="me-3">
<a class="text-muted text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx" ngbPopover="GitHub" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
{{ versionString }}
</a> </a>
</div> </div>
<div *ngIf="!settingsService.updateCheckingIsSet || appRemoteVersion" class="version-check"> </li>
<ng-template #updateAvailablePopContent> </ul>
<span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span> </nav>
</ng-template>
<ng-template #updateCheckingNotEnabledPopContent> <div class="container-fluid">
<p class="small mb-2"> <div class="row">
<ng-container i18n>Paperless-ngx can automatically check for updates</ng-container> <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">
</p> <button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
<div class="btn-group btn-group-xs flex-fill w-100"> <svg class="sidebaricon-sm" fill="currentColor">
<button class="btn btn-outline-primary" (click)="setUpdateChecking(true)">Enable</button> @if (slimSidebarEnabled) {
<button class="btn btn-outline-secondary" (click)="setUpdateChecking(false)">Disable</button> <use xlink:href="assets/bootstrap-icons.svg#chevron-double-right"/>
</div> }
<p class="small mb-0 mt-2"> @if (!slimSidebarEnabled) {
<a class="small text-decoration-none fst-italic" routerLink="/settings" fragment="update-checking" i18n> <use xlink:href="assets/bootstrap-icons.svg#chevron-double-left"/>
}
</svg>
</button>
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#house"/>
</svg><span>&nbsp;<ng-container i18n>Dashboard</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#files"/>
</svg><span>&nbsp;<ng-container i18n>Documents</ng-container></span>
</a>
</li>
</ul>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
@if (savedViewService.loading || savedViewService.sidebarViews?.length > 0) {
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted">
<span i18n>Saved views</span>
@if (savedViewService.loading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
}
</h6>
}
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
@for (view of savedViewService.sidebarViews; track view) {
<li class="nav-item w-100"
cdkDrag
[cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
cdkDragPreviewContainer="parent"
cdkDragPreviewClass="navItemDrag"
(cdkDragStarted)="onDragStart($event)"
(cdkDragEnded)="onDragEnd($event)">
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
</svg><span>&nbsp;{{view.name}}</span>
</a>
@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">
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/>
</svg>
</div>
}
</li>
}
</ul>
</ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
@if (openDocuments.length > 0) {
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted">
<span i18n>Open documents</span>
</h6>
}
<ul class="nav flex-column mb-2">
@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">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg><span>&nbsp;{{d.title | documentTitle}}</span>
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
<svg fill="currentColor" class="toolbaricon">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
</span>
</a>
</li>
}
@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">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg><span>&nbsp;<ng-container i18n>Close all</ng-container></span>
</a>
</li>
}
</ul>
</ng-container>
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted">
<span i18n>Manage</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person"/>
</svg><span>&nbsp;<ng-container i18n>Correspondents</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }" tourAnchor="tour.tags">
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
</svg><span>&nbsp;<ng-container i18n>Tags</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
</svg><span>&nbsp;<ng-container i18n>Document Types</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
</svg><span>&nbsp;<ng-container i18n>Storage Paths</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#ui-radios"/>
</svg><span>&nbsp;<ng-container i18n>Custom Fields</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.ConsumptionTemplate }" tourAnchor="tour.consumption-templates">
<a class="nav-link" routerLink="templates" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Consumption templates" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-ruled"/>
</svg><span>&nbsp;<ng-container i18n>Templates</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }" tourAnchor="tour.mail">
<a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#envelope"/>
</svg><span>&nbsp;<ng-container i18n>Mail</ng-container></span>
</a>
</li>
</ul>
<h6 class="sidebar-heading px-3 mt-auto pt-4 mb-1 text-muted">
<span i18n>Administration</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }" tourAnchor="tour.settings">
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
</svg><span>&nbsp;<ng-container i18n>Settings</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people"/>
</svg><span>&nbsp;<ng-container i18n>Users & Groups</ng-container></span>
</a>
</li>
<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">
@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">
<use xlink:href="assets/bootstrap-icons.svg#list-task"/>
</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>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
</svg><span>&nbsp;<ng-container i18n>Logs</ng-container></span>
</a>
</li>
<li class="nav-item mt-2" tourAnchor="tour.outro">
<a class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#question-circle"/>
</svg><span class="ms-1">&nbsp;<ng-container i18n>Documentation</ng-container></span>
</a>
</li>
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
<div class="px-3 py-0 text-muted small d-flex align-items-center flex-wrap">
<div class="me-3">
<a class="text-muted text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx" ngbPopover="GitHub" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
{{ versionString }}
</a>
</div>
@if (!settingsService.updateCheckingIsSet || appRemoteVersion) {
<div class="version-check">
<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>
</ng-template>
<ng-template #updateCheckingNotEnabledPopContent>
<p class="small mb-2">
<ng-container i18n>Paperless-ngx can automatically check for updates</ng-container>
</p>
<div class="btn-group btn-group-xs flex-fill w-100">
<button class="btn btn-outline-primary" (click)="setUpdateChecking(true)">Enable</button>
<button class="btn btn-outline-secondary" (click)="setUpdateChecking(false)">Disable</button>
</div>
<p class="small mb-0 mt-2">
<a class="small text-decoration-none fst-italic" routerLink="/settings" fragment="update-checking" i18n>
How does this work? How does this work?
</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) {
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> <a class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases"
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> [ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" /> <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
</svg> <use xlink:href="assets/bootstrap-icons.svg#info-circle" />
<ng-container *ngIf="appRemoteVersion?.update_available" i18n>Update available</ng-container> </svg>
</a> @if (appRemoteVersion?.update_available) {
</ng-container> <ng-container i18n>Update available</ng-container>
<ng-template #updateCheckNotSet> }
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking" </a>
[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"> } @else {
<use xlink:href="assets/bootstrap-icons.svg#info-circle" /> <a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
</svg> [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter" container="body">
</a> <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
</ng-template> <use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</div> </svg>
</div> </a>
</li> }
</ul> </div>
</div> }
</nav> </div>
</li>
</ul>
</div>
</nav>
<main role="main" class="ms-sm-auto px-md-4" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'col-md-9 col-lg-10 col-xxxl-11'"> <main role="main" class="ms-sm-auto px-md-4" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'col-md-9 col-lg-10 col-xxxl-11'">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</main> </main>
</div> </div>
</div> </div>

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

@ -1,24 +1,32 @@
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4> <h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()"> <button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
</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>
</div> }
<div class="modal-footer"> @if (message) {
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n> <p class="mb-0" [innerHTML]="message | safeHtml"></p>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>
<span class="d-inline-block" style="padding-bottom: 1px;" >Cancel</span> <span class="d-inline-block" style="padding-bottom: 1px;" >Cancel</span>
</button> </button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled"> <button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
<span> <span>
{{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) {
</button> <ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
<button *ngIf="alternativeBtnCaption" type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled"> }
{{alternativeBtnCaption}} </button>
</button> @if (alternativeBtnCaption) {
</div> <button type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled">
{{alternativeBtnCaption}}
</button>
}
</div>

View File

@ -1,15 +1,18 @@
<div class="btn-group w-100" ngbDropdown role="group"> <div class="btn-group w-100" ngbDropdown role="group">
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled"> <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
{{title}} {{title}}
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span> <pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
</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) {
<use xlink:href="assets/bootstrap-icons.svg#check"/> <svg fill="currentColor" class="buttonicon-sm">
</svg> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</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,52 +25,57 @@
</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 class="input-group input-group-sm">
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
</svg>
</button>
</div>
</div> </div>
<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="input-group input-group-sm">
<div i18n>Before</div> <input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
<a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()"> maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
</svg>
</button>
</div>
</div>
<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 i18n>Before</div>
@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 class="input-group input-group-sm">
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
</svg>
</button>
</div>
</div> </div>
<div class="input-group input-group-sm">
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
</svg>
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,93 +1,95 @@
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off"> <form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">
<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>
</div>
<div class="col">
<pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
</div>
</div> </div>
<div class="row"> <div class="col">
<div class="col-md-4"> <pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
<h5 class="border-bottom pb-2" i18n>Filters</h5> </div>
<p class="small" i18n>Process documents that match <em>all</em> filters specified below.</p> </div>
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select> <div class="row">
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text> <div class="col-md-4">
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text> <h5 class="border-bottom pb-2" i18n>Filters</h5>
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select> <p class="small" i18n>Process documents that match <em>all</em> filters specified below.</p>
</div> <pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
<div class="col"> <pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
<div class="row"> <pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text>
<div class="col"> <pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
<h5 class="border-bottom pb-2" i18n>Assignments</h5> </div>
</div> <div class="col">
<div class="row">
<div class="col">
<h5 class="border-bottom pb-2" i18n>Assignments</h5>
</div> </div>
<div class="row"> </div>
<div class="col"> <div class="row">
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#consumption-templates'>documentation</a>." [error]="error?.assign_title"></pngx-input-text> <div class="col">
<pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags> <pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#consumption-templates'>documentation</a>." [error]="error?.assign_title"></pngx-input-text>
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select> <pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></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 storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select> <pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
<pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select> <pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select>
<pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select>
</div> </div>
<div class="col"> <div class="col">
<pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select> <pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
<div> <div>
<label class="form-label" i18n>Assign view permissions</label> <label class="form-label" i18n>Assign view permissions</label>
<div class="mb-2"> <div class="mb-2">
<div class="row mb-1"> <div class="row mb-1">
<div class="col-lg-3"> <div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label> <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
</div>
</div> </div>
<div class="row"> <div class="col-lg-9">
<div class="col-lg-3"> <pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
</div>
</div> </div>
</div> </div>
<label class="form-label" i18n>Assign edit permissions</label> <div class="row">
<div> <div class="col-lg-3">
<div class="row mb-1"> <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
</div>
</div> </div>
<div class="row"> <div class="col-lg-9">
<div class="col-lg-3"> <pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
</div>
</div> </div>
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
</div> </div>
</div> </div>
<label class="form-label" i18n>Assign edit permissions</label>
<div>
<div class="row mb-1">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
</div>
</div>
<div class="row">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
</div>
</div>
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
<div class="modal-footer"> </div>
<span class="text-danger" *ngIf="error?.non_field_errors"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span> <div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> @if (error?.non_field_errors) {
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> <span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
</div> }
</form> <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>
</div>
</form>

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

@ -1,25 +1,29 @@
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off"> <form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="col">
<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-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
</div>
<div *pngxIfOwner="object">
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form>
</div>
<div class="col">
<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>
@if (patternRequired) {
<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 class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> <div *pngxIfOwner="object">
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> <pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form>
</div> </div>
</form>
</div>
<div class="modal-footer">
<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>
</div>
</form>

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

@ -1,26 +1,30 @@
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off"> <form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
</button> </button>
</div>
<div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></pngx-input-color>
<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>
@if (patternRequired) {
<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">
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form>
</div> </div>
<div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></pngx-input-color> </div>
<div class="modal-footer">
<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> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
<pngx-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text> </div>
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check> </form>
<div *pngxIfOwner="object">
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form>
</div>
</div>
<div class="modal-footer">
<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>
</div>
</form>

View File

@ -4,49 +4,61 @@
<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="btn-group btn-group-xs flex-fill" role="group"> <div class="list-group-item d-flex">
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd_{{name}}" name="logicalOperatorAnd_{{name}}" value="and"> <div class="btn-group btn-group-xs flex-fill" role="group">
<label class="btn btn-outline-primary" for="logicalOperatorAnd_{{name}}" i18n>All</label> <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="logicalOperatorOr_{{name}}" name="logicalOperatorOr_{{name}}" value="or"> <label class="btn btn-outline-primary" for="logicalOperatorAnd_{{name}}" i18n>All</label>
<label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label> <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr_{{name}}" name="logicalOperatorOr_{{name}}" value="or">
<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="btn-group btn-group-xs flex-fill" role="group"> <div class="list-group-item d-flex">
<input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionInclude_{{name}}" name="intersectionInclude_{{name}}" value="include"> <div class="btn-group btn-group-xs flex-fill" role="group">
<label class="btn btn-outline-primary" for="intersectionInclude_{{name}}" i18n>Include</label> <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="intersectionExclude_{{name}}" name="intersectionExclude_{{name}}" value="exclude"> <label class="btn btn-outline-primary" for="intersectionInclude_{{name}}" i18n>Include</label>
<label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label> <input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionExclude_{{name}}" name="intersectionExclude_{{name}}" value="exclude">
<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>
<pngx-toggleable-dropdown-button @for (item of selectionModel.itemsSorted | filter: filterText; track item; let i = $index) {
*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"> @if (allowSelectNone || item.id) {
</pngx-toggleable-dropdown-button> <pngx-toggleable-dropdown-button
</ng-container> [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">
</div> </pngx-toggleable-dropdown-button>
<button *ngIf="editing" 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> }
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor"> </div>
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" /> }
</svg> @if (editing) {
</button> <button class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
<div *ngIf="!editing && manyToOne" class="list-group-item list-group-item-note pt-1 pb-2"> <small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
<small i18n>Click again to exclude items.</small> <svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
</div> <use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
</svg>
</button>
}
@if (!editing && manyToOne) {
<div class="list-group-item list-group-item-note pt-1 pb-2">
<small i18n>Click again to exclude items.</small>
</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,19 +1,27 @@
<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) {
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> <div class="d-flex align-items-center position-relative hidden-button-container col-md-3">
<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>
<svg class="sidebaricon" fill="currentColor"> @if (removable) {
<use xlink:href="assets/bootstrap-icons.svg#x"/> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
</svg>&nbsp;<ng-container i18n>Remove</ng-container> <svg class="sidebaricon" fill="currentColor">
</button> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</div> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
<div [ngClass]="{'col-md-9': horizontal, 'align-items-center': horizontal, 'd-flex': horizontal}"> </button>
<div class="form-check"> }
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled"> </div>
<label *ngIf="!horizontal" class="form-check-label" [for]="inputId">{{title}}</label> }
<div *ngIf="hint" class="form-text text-muted">{{hint}}</div> <div [ngClass]="{'col-md-9': horizontal, 'align-items-center': horizontal, 'd-flex': horizontal}">
<div class="form-check">
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
@if (!horizontal) {
<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> </div>
</div>

View File

@ -1,28 +1,32 @@
<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>
<ng-template #popContent> <ng-template #popContent>
<div style="min-width: 200px;" class="pb-3"> <div style="min-width: 200px;" class="pb-3">
<color-slider [color]="value" (onChangeComplete)="colorChanged($event.color.hex)"></color-slider> <color-slider [color]="value" (onChangeComplete)="colorChanged($event.color.hex)"></color-slider>
</div> </div>
</ng-template> </ng-template>
<input #inputField class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow"> <input #inputField class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
<button class="btn btn-outline-secondary" type="button" (click)="randomize()"> <button class="btn btn-outline-secondary" type="button" (click)="randomize()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dice-5" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dice-5" viewBox="0 0 16 16">
<path d="M13 1a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10zM3 0a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V3a3 3 0 0 0-3-3H3z"/> <path d="M13 1a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10zM3 0a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V3a3 3 0 0 0-3-3H3z"/>
<path d="M5.5 4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm-8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm4-4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/> <path d="M5.5 4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm-8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm4-4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
</svg> </svg>
</button> </button>
</div> </div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small> @if (hint) {
<div class="invalid-feedback"> <small class="form-text text-muted">{{hint}}</small>
{{error}} }
</div> <div class="invalid-feedback">
{{error}}
</div>
</div> </div>

View File

@ -2,36 +2,44 @@
<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) {
<svg class="sidebaricon" fill="currentColor"> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <svg class="sidebaricon" fill="currentColor">
</svg>&nbsp;<ng-container i18n>Remove</ng-container> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</button> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</div> </button>
<div class="position-relative" [class.col-md-9]="horizontal"> }
<div class="input-group" [class.is-invalid]="error"> </div>
<input #inputField class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10" <div class="position-relative" [class.col-md-9]="horizontal">
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)" <div class="input-group" [class.is-invalid]="error">
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled"> <input #inputField class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled"> (dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="buttonicon"> name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled">
<use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use> <button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled">
</svg> <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="buttonicon">
</button> <use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use>
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ fitlerButtonTitle }}"> </svg>
<svg class="buttonicon" fill="currentColor"> </button>
<use xlink:href="assets/bootstrap-icons.svg#filter" /> @if (showFilter) {
</svg> <button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ filterButtonTitle }}">
</button> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>
</button>
}
</div>
<div class="invalid-feedback position-absolute top-100" i18n>Invalid date.</div>
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
@if (getSuggestions().length > 0) {
<small>
<span i18n>Suggestions:</span>&nbsp;
@for (s of getSuggestions(); track s) {
<a (click)="onSuggestionClick(s)" [routerLink]="[]">{{s}}</a>&nbsp;
}
</small>
}
</div> </div>
<div class="invalid-feedback position-absolute top-100" i18n>Invalid date.</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
<small *ngIf="getSuggestions().length > 0">
<span i18n>Suggestions:</span>&nbsp;
<ng-container *ngFor="let s of getSuggestions()">
<a (click)="onSuggestionClick(s)" [routerLink]="[]">{{s}}</a>&nbsp;
</ng-container>
</small>
</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>
@ -23,28 +27,30 @@
[loading]="loading" [loading]="loading"
[typeahead]="documentsInput$" [typeahead]="documentsInput$"
(change)="onChange(selectedDocuments)"> (change)="onChange(selectedDocuments)">
<ng-template ng-label-tmp let-document="item"> <ng-template ng-label-tmp let-document="item">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<svg class="sidebaricon" fill="currentColor" xmlns="http://www.w3.org/2000/svg" (click)="unselect(document)"> <svg class="sidebaricon" fill="currentColor" xmlns="http://www.w3.org/2000/svg" (click)="unselect(document)">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();"> <a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();">
<svg class="sidebaricon-sm me-1" fill="currentColor"> <svg class="sidebaricon-sm me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/> <use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg><span>{{document.title}}</span> </svg><span>{{document.title}}</span>
</a> </a>
</div> </div>
</ng-template> </ng-template>
<ng-template ng-loadingspinner-tmp> <ng-template ng-loadingspinner-tmp>
<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-template> </ng-template>
<ng-template ng-option-tmp let-document="item" let-index="index" let-search="searchTerm"> <ng-template ng-option-tmp let-document="item" let-index="index" let-search="searchTerm">
<div>{{document.title}} <small class="text-muted">({{document.created | customDate:'shortDate'}})</small></div> <div>{{document.title}} <small class="text-muted">({{document.created | customDate:'shortDate'}})</small></div>
</ng-template> </ng-template>
</ng-select> </ng-select>
</div>
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
</div> </div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
</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) {
<svg class="sidebaricon" fill="currentColor"> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <svg class="sidebaricon" fill="currentColor">
</svg>&nbsp;<ng-container i18n>Remove</ng-container> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</button> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</div> </button>
<div class="position-relative" [class.col-md-9]="horizontal"> }
<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">
<button *ngIf="showAdd" 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="position-relative" [class.col-md-9]="horizontal">
{{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">
@if (showAdd) {
<button class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="disabled">+1</button>
}
</div>
<div class="invalid-feedback position-absolute top-100">
{{error}}
</div>
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
</div> </div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
</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) {
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> <button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
<use xlink:href="assets/bootstrap-icons.svg#eye" /> <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
</svg> <use xlink:href="assets/bootstrap-icons.svg#eye" />
</button> </svg>
</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,68 +1,75 @@
<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>
<ng-container [ngTemplateOutlet]="permissionsForm"></ng-container> <button ngbAccordionButton i18n>Edit Permissions</button>
</ng-template> </h2>
</ngb-panel> <div ngbAccordionCollapse>
</ngb-accordion> <div ngbAccordionBody>
</ng-container> <ng-template>
<ng-container [ngTemplateOutlet]="permissionsForm"></ng-container>
</ng-template>
</div>
</div>
</div>
</div>
}
<ng-template #permissionsForm> <ng-template #permissionsForm>
<div [formGroup]="form"> <div [formGroup]="form">
<div class="row"> <div class="row">
<div class="col-lg-3"> <div class="col-lg-3">
<label class="form-label d-block my-2" i18n>Owner:</label> <label class="form-label d-block my-2" i18n>Owner:</label>
</div>
<div class="col-lg-9">
<pngx-input-select [items]="users" bindLabel="username" formControlName="owner" [allowNull]="true"></pngx-input-select>
</div>
</div> </div>
<div class="col-lg-9"> <small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
<pngx-input-select [items]="users" bindLabel="username" formControlName="owner" [allowNull]="true"></pngx-input-select> <div formGroupName="set_permissions">
<h6 class="mt-3" i18n>View</h6>
<div formGroupName="view" class="mb-2">
<div class="row mb-1">
<div class="col-lg-3">
<label class="form-label d-block my-2" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="view" formControlName="users"></pngx-permissions-user>
</div>
</div>
<div class="row">
<div class="col-lg-3">
<label class="form-label d-block my-2" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="view" formControlName="groups"></pngx-permissions-group>
</div>
</div>
</div>
<h6 class="mt-4" i18n>Edit</h6>
<div formGroupName="change">
<div class="row mb-1">
<div class="col-lg-3">
<label class="form-label d-block my-2" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="change" formControlName="users"></pngx-permissions-user>
</div>
</div>
<div class="row">
<div class="col-lg-3">
<label class="form-label d-block my-2" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="change" formControlName="groups"></pngx-permissions-group>
</div>
</div>
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
</div>
</div> </div>
</div> </div>
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
<div formGroupName="set_permissions">
<h6 class="mt-3" i18n>View</h6>
<div formGroupName="view" class="mb-2">
<div class="row mb-1">
<div class="col-lg-3">
<label class="form-label d-block my-2" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="view" formControlName="users"></pngx-permissions-user>
</div>
</div>
<div class="row">
<div class="col-lg-3">
<label class="form-label d-block my-2" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="view" formControlName="groups"></pngx-permissions-group>
</div>
</div>
</div>
<h6 class="mt-4" i18n>Edit</h6>
<div formGroupName="change">
<div class="row mb-1">
<div class="col-lg-3">
<label class="form-label d-block my-2" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="change" formControlName="users"></pngx-permissions-user>
</div>
</div>
<div class="row">
<div class="col-lg-3">
<label class="form-label d-block my-2" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="change" formControlName="groups"></pngx-permissions-group>
</div>
</div>
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
</div>
</div>
</div>
</ng-template> </ng-template>

View File

@ -1,57 +1,69 @@
<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>
<svg class="sidebaricon" fill="currentColor"> }
<use xlink:href="assets/bootstrap-icons.svg#x"/> @if (removable) {
</svg>&nbsp;<ng-container i18n>Remove</ng-container> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
</button> <svg class="sidebaricon" fill="currentColor">
</div> <use xlink:href="assets/bootstrap-icons.svg#x"/>
<div [class.col-md-9]="horizontal"> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
<div [class.input-group]="allowCreateNew || showFilter" [class.is-invalid]="error"> </button>
<ng-select name="inputId" [(ngModel)]="value" }
[disabled]="disabled"
[style.color]="textColor"
[style.background]="backgroundColor"
[class.private]="isPrivate"
[clearable]="allowNull"
[items]="items"
[addTag]="allowCreateNew && addItemRef"
addTagText="Add item"
i18n-addTagText="Used for both types, correspondents, storage paths"
[placeholder]="placeholder"
[notFoundText]="notFoundText"
[multiple]="multiple"
[bindLabel]="bindLabel"
bindValue="id"
(change)="onChange(value)"
(search)="onSearch($event)"
(focus)="clearLastSearchTerm()"
(clear)="clearLastSearchTerm()"
(blur)="onBlur()">
</ng-select>
<button *ngIf="allowCreateNew" class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
</button>
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" title="{{ filterButtonTitle }}">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>
</button>
</div> </div>
<div class="invalid-feedback"> <div [class.col-md-9]="horizontal">
{{error}} <div [class.input-group]="allowCreateNew || showFilter" [class.is-invalid]="error">
<ng-select name="inputId" [(ngModel)]="value"
[disabled]="disabled"
[style.color]="textColor"
[style.background]="backgroundColor"
[class.private]="isPrivate"
[clearable]="allowNull"
[items]="items"
[addTag]="allowCreateNew && addItemRef"
addTagText="Add item"
i18n-addTagText="Used for both types, correspondents, storage paths"
[placeholder]="placeholder"
[notFoundText]="notFoundText"
[multiple]="multiple"
[bindLabel]="bindLabel"
bindValue="id"
(change)="onChange(value)"
(search)="onSearch($event)"
(focus)="clearLastSearchTerm()"
(clear)="clearLastSearchTerm()"
(blur)="onBlur()">
</ng-select>
@if (allowCreateNew) {
<button class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
</button>
}
@if (showFilter) {
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" title="{{ filterButtonTitle }}">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>
</button>
}
</div>
<div class="invalid-feedback">
{{error}}
</div>
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
@if (getSuggestions().length > 0) {
<small>
<span i18n>Suggestions:</span>&nbsp;
@for (s of getSuggestions(); track s) {
<a (click)="value = s.id; onChange(value)" [routerLink]="[]">{{s.name}}</a>&nbsp;
}
</small>
}
</div> </div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
<small *ngIf="getSuggestions().length > 0">
<span i18n>Suggestions:</span>&nbsp;
<ng-container *ngFor="let s of getSuggestions()">
<a (click)="value = s.id; onChange(value)" [routerLink]="[]">{{s.name}}</a>&nbsp;
</ng-container>
</small>
</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) {
<svg class="buttonicon" fill="currentColor"> <button class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled">
<use xlink:href="assets/bootstrap-icons.svg#plus" /> <svg class="buttonicon" fill="currentColor">
</svg> <use xlink:href="assets/bootstrap-icons.svg#plus" />
</button> </svg>
<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"> </button>
<svg class="buttonicon" fill="currentColor"> }
<use xlink:href="assets/bootstrap-icons.svg#filter" /> @if (showFilter) {
</svg> <button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="hasPrivate || this.value === null" i18n-title title="Filter documents with these Tags">
</button> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>
</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>
<span i18n>Suggestions:</span>&nbsp; }
<ng-container *ngFor="let tag of getSuggestions()"> @if (getSuggestions().length > 0) {
<a (click)="addTag(tag.id)" [routerLink]="[]">{{tag?.name}}</a>&nbsp; <small class="position-absolute top-100">
</ng-container> <span i18n>Suggestions:</span>&nbsp;
</small> @for (tag of getSuggestions(); track tag) {
<a (click)="addTag(tag.id)" [routerLink]="[]">{{tag?.name}}</a>&nbsp;
}
</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,18 +2,22 @@
<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) {
<svg class="sidebaricon" fill="currentColor"> <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <svg class="sidebaricon" fill="currentColor">
</svg>&nbsp;<ng-container i18n>Remove</ng-container> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</button> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</div> </button>
<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"> </div>
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small> <div class="position-relative" [class.col-md-9]="horizontal">
<div class="invalid-feedback position-absolute top-100"> <input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
{{error}} @if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
<div class="invalid-feedback position-absolute top-100">
{{error}}
</div>
</div> </div>
</div> </div>
</div> </div>
</div>

View File

@ -1,12 +1,14 @@
<div class="mb-3" [class.pb-3]="error"> <div class="mb-3" [class.pb-3]="error">
<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

@ -1,22 +1,24 @@
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4> <h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancelClicked()"> <button type="button" class="btn-close" aria-label="Close" (click)="cancelClicked()">
</button> </button>
</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>
</form> </form>
</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

@ -1,92 +1,106 @@
<div class="btn-group w-100" ngbDropdown role="group"> <div class="btn-group w-100" ngbDropdown role="group">
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="isActive ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled"> <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="isActive ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
<svg class="toolbaricon" fill="currentColor"> <svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" /> <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
</svg> </svg>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div> <div class="d-none d-sm-inline">&nbsp;{{title}}</div>
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span> <pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
</button> </button>
<div class="dropdown-menu permission-filter-dropdown shadow py-0 w-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="dropdown-menu permission-filter-dropdown shadow py-0 w-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<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) {
<use xlink:href="assets/bootstrap-icons.svg#check"/> <svg fill="currentColor" class="buttonicon-sm">
</svg> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</div> </svg>
<div class="me-1"> }
<small i18n>All</small>
</div>
</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">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.SELF" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<div class="me-1">
<small i18n>My documents</small>
</div>
</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">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.NOT_SELF" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<div class="me-1">
<small i18n>Shared with me</small>
</div>
</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">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<div class="me-1">
<small i18n>Shared by me</small>
</div>
</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">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.UNOWNED" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<div class="me-1">
<small i18n>Unowned</small>
</div>
</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">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.OTHERS" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<div class="me-1 w-100">
<ng-select
name="user"
class="user-select small"
[(ngModel)]="selectionModel.includeUsers"
[disabled]="disabled"
[clearable]="false"
[items]="users"
bindLabel="username"
multiple="true"
bindValue="id"
placeholder="Users"
i18n-placeholder
(change)="onUserSelect()">
</ng-select>
</div>
</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">
<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">
<label class="form-check-label w-100" for="hideUnowned"><small i18n>Hide unowned</small></label>
</div>
</div>
</div> </div>
<div class="me-1">
<small i18n>All</small>
</div>
</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">
<div class="selected-icon me-1">
@if (selectionModel.ownerFilter === OwnerFilterType.SELF) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
}
</div>
<div class="me-1">
<small i18n>My documents</small>
</div>
</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">
<div class="selected-icon me-1">
@if (selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
}
</div>
<div class="me-1">
<small i18n>Shared with me</small>
</div>
</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">
<div class="selected-icon me-1">
@if (selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
}
</div>
<div class="me-1">
<small i18n>Shared by me</small>
</div>
</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">
<div class="selected-icon me-1">
@if (selectionModel.ownerFilter === OwnerFilterType.UNOWNED) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
}
</div>
<div class="me-1">
<small i18n>Unowned</small>
</div>
</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">
<div class="selected-icon me-1">
@if (selectionModel.ownerFilter === OwnerFilterType.OTHERS) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
}
</div>
<div class="me-1 w-100">
<ng-select
name="user"
class="user-select small"
[(ngModel)]="selectionModel.includeUsers"
[disabled]="disabled"
[clearable]="false"
[items]="users"
bindLabel="username"
multiple="true"
bindValue="id"
placeholder="Users"
i18n-placeholder
(change)="onUserSelect()">
</ng-select>
</div>
</button>
@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">
<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>
</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) {
<div class="col-3">{{type.key}}:</div> <li class="list-group-item d-flex" [formGroupName]="type.key">
<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> }
<div *ngIf="error" class="invalid-feedback d-block">{{error}}</div> </li>
}
@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) {
<p class="fst-italic position-absolute top-50 start-50 translate-middle" i18n>Error loading preview</p> <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>
</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 {
<svg width="2em" height="2em" fill="currentColor" class="position-absolute top-50 start-50 translate-middle"> @if (requiresPassword) {
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-lock"/> <div class="w-100 h-100 position-relative">
</svg> <svg width="2em" height="2em" fill="currentColor" class="position-absolute top-50 start-50 translate-middle">
</div> <use xlink:href="assets/bootstrap-icons.svg#file-earmark-lock"/>
<pngx-pdf-viewer *ngIf="!requiresPassword" </svg>
[src]="previewURL" </div>
[original-size]="false" }
[show-borders]="true" @if (!requiresPassword) {
[show-all]="true" <pngx-pdf-viewer
(error)="onError($event)"> [src]="previewURL"
</pngx-pdf-viewer> [original-size]="false"
</ng-template> [show-borders]="true"
</ng-template> [show-all]="true"
(error)="onError($event)">
</pngx-pdf-viewer>
}
}
}
</div> </div>

View File

@ -1,53 +1,57 @@
<form [formGroup]="form" (ngSubmit)="save()" autocomplete="off"> <form [formGroup]="form" (ngSubmit)="save()" autocomplete="off">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title" i18n>Edit Profile</h4> <h4 class="modal-title" id="modal-basic-title" i18n>Edit Profile</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()"> <button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
</button> </button>
</div>
<div class="modal-body">
<pngx-input-text i18n-title title="Email" formControlName="email" (keyup)="onEmailKeyUp($event)" [error]="error?.email"></pngx-input-text>
<div ngbAccordion>
<div ngbAccordionItem="first" [collapsed]="!showEmailConfirm" class="border-0 bg-transparent">
<div ngbAccordionCollapse>
<div ngbAccordionBody class="p-0 pb-3">
<pngx-input-text i18n-title title="Confirm Email" formControlName="email_confirm" (keyup)="onEmailConfirmKeyUp($event)" autocomplete="email" [error]="error?.email_confirm"></pngx-input-text>
</div>
</div>
</div>
</div> </div>
<div class="modal-body"> <pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [showReveal]="true" autocomplete="current-password" [error]="error?.password"></pngx-input-password>
<pngx-input-text i18n-title title="Email" formControlName="email" (keyup)="onEmailKeyUp($event)" [error]="error?.email"></pngx-input-text> <div ngbAccordion>
<div ngbAccordion> <div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm" class="border-0 bg-transparent">
<div ngbAccordionItem="first" [collapsed]="!showEmailConfirm" class="border-0 bg-transparent"> <div ngbAccordionCollapse>
<div ngbAccordionCollapse> <div ngbAccordionBody class="p-0 pb-3">
<div ngbAccordionBody class="p-0 pb-3"> <pngx-input-password i18n-title title="Confirm Password" formControlName="password_confirm" (keyup)="onPasswordConfirmKeyUp($event)" autocomplete="new-password" [error]="error?.password_confirm"></pngx-input-password>
<pngx-input-text i18n-title title="Confirm Email" formControlName="email_confirm" (keyup)="onEmailConfirmKeyUp($event)" autocomplete="email" [error]="error?.email_confirm"></pngx-input-text>
</div>
</div>
</div> </div>
</div> </div>
<pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [showReveal]="true" autocomplete="current-password" [error]="error?.password"></pngx-input-password> </div>
<div ngbAccordion> </div>
<div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm" class="border-0 bg-transparent"> <pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
<div ngbAccordionCollapse> <pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
<div ngbAccordionBody class="p-0 pb-3"> <div class="mb-3">
<pngx-input-password i18n-title title="Confirm Password" formControlName="password_confirm" (keyup)="onPasswordConfirmKeyUp($event)" autocomplete="new-password" [error]="error?.password_confirm"></pngx-input-password> <label class="form-label" i18n>API Auth Token</label>
</div> <div class="position-relative">
</div> <div class="input-group">
<input type="text" class="form-control" formControlName="auth_token" readonly>
<button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
<svg class="buttonicon-sm" fill="currentColor">
@if (!copied) {
<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>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-repeat" />
</svg>
</button>
</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" i18n>Copied!</span>
</div> </div>
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text> <div class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div>
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text> </div>
<div class="mb-3">
<label class="form-label" i18n>API Auth Token</label>
<div class="position-relative">
<div class="input-group">
<input type="text" class="form-control" formControlName="auth_token" readonly>
<button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
<svg class="buttonicon-sm" fill="currentColor">
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
</svg><span class="visually-hidden" i18n>Copy</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-repeat" />
</svg>
</button>
</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" i18n>Copied!</span>
</div>
<div class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div>
</div>
</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

@ -1,61 +1,79 @@
<div ngbDropdown> <div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" id="shareLinksDropdown" [disabled]="disabled" ngbDropdownToggle> <button class="btn btn-sm btn-outline-primary" id="shareLinksDropdown" [disabled]="disabled" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor"> <svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#link" /> <use xlink:href="assets/bootstrap-icons.svg#link" />
</svg> </svg>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Share Links</ng-container></div> <div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Share Links</ng-container></div>
</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) {
{{ getDaysRemaining(link) }} <span class="input-group-text">
</span> {{ getDaysRemaining(link) }}
</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" />
</svg><span class="visually-hidden" i18n>Copy</span> }
</button> @if (copied === link.id) {
<button *ngIf="canShare(link)" type="button" class="btn btn-sm btn-outline-primary" (click)="share(link)"> <use xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
<svg class="buttonicon" fill="currentColor"> }
<use xlink:href="assets/bootstrap-icons.svg#box-arrow-up" /> </svg><span class="visually-hidden" i18n>Copy</span>
</svg><span class="visually-hidden" i18n>Share</span> </button>
</button> @if (canShare(link)) {
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete(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" />
</svg><span class="visually-hidden" i18n>Share</span>
</button>
}
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete(link)">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg><span class="visually-hidden" i18n>Delete</span> </svg><span class="visually-hidden" i18n>Delete</span>
</button> </button>
</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"> }
<div class="input-group input-group-sm w-100"> <li class="list-group-item pt-3 pb-2">
<div class="form-check form-switch ms-auto small"> <div class="input-group input-group-sm w-100">
<input class="form-check-input" type="checkbox" role="switch" id="versionSwitch" [disabled]="!hasArchiveVersion" [(ngModel)]="useArchiveVersion"> <div class="form-check form-switch ms-auto small">
<label class="form-check-label" for="versionSwitch" i18n>Share archive version</label> <input class="form-check-input" type="checkbox" role="switch" id="versionSwitch" [disabled]="!hasArchiveVersion" [(ngModel)]="useArchiveVersion">
</div> <label class="form-check-label" for="versionSwitch" i18n>Share archive version</label>
</div> </div>
<div class="input-group input-group-sm w-100 mt-2"> </div>
<label class="input-group-text" for="addLink"><ng-container i18n>Expires</ng-container>:</label> <div class="input-group input-group-sm w-100 mt-2">
<select class="form-select form-select-sm" [(ngModel)]="expirationDays"> <label class="input-group-text" for="addLink"><ng-container i18n>Expires</ng-container>:</label>
<option *ngFor="let option of EXPIRATION_OPTIONS" [ngValue]="option.value">{{ option.label }}</option> <select class="form-select form-select-sm" [(ngModel)]="expirationDays">
</select> @for (option of EXPIRATION_OPTIONS; track option) {
<button class="btn btn-sm btn-outline-primary ms-auto" type="button" (click)="createLink()" [disabled]="loading"> <option [ngValue]="option.value">{{ option.label }}</option>
<div *ngIf="loading" class="spinner-border spinner-border-sm me-2" role="status"></div> }
<svg *ngIf="!loading" class="buttonicon me-1" fill="currentColor"> </select>
<use xlink:href="assets/bootstrap-icons.svg#plus" /> <button class="btn btn-sm btn-outline-primary ms-auto" type="button" (click)="createLink()" [disabled]="loading">
</svg> @if (loading) {
<ng-container i18n>Create</ng-container> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
</button> }
</div> @if (!loading) {
</li> <svg class="buttonicon me-1" fill="currentColor">
</ul> <use xlink:href="assets/bootstrap-icons.svg#plus" />
</div> </svg>
</div> }
<ng-container i18n>Create</ng-container>
</button>
</div>
</li>
</ul>
</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,43 +1,58 @@
<ngb-toast @for (toast of toasts; track toast) {
*ngFor="let toast of toasts" <ngb-toast
[autohide]="true" [delay]="toast.delay" [autohide]="true" [delay]="toast.delay"
[class]="toast.classname" [class]="toast.classname"
[class.mb-2]="true" [class.mb-2]="true"
(shown)="onShow(toast)" (shown)="onShow(toast)"
(hidden)="toastService.closeToast(toast)"> (hidden)="toastService.closeToast(toast)">
<ngb-progressbar class="position-absolute h-100 w-100 top-90 start-0 bottom-0 end-0 pe-none" type="dark" [max]="toast.delay" [value]="toast.delayRemaining"></ngb-progressbar> <ngb-progressbar class="position-absolute h-100 w-100 top-90 start-0 bottom-0 end-0 pe-none" type="dark" [max]="toast.delay" [value]="toast.delayRemaining"></ngb-progressbar>
<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"/>
</svg> }
<div> @if (toast.error) {
<p class="mb-0">{{toast.content}}</p> <use xlink:href="assets/bootstrap-icons.svg#exclamation-triangle"/>
<details *ngIf="toast.error"> }
<div class="mt-2"> </svg>
<dl class="row mb-0" *ngIf="isDetailedError(toast.error)"> <div>
<dt class="col-sm-3 fw-normal text-end">URL</dt> <p class="mb-0">{{toast.content}}</p>
<dd class="col-sm-9">{{ toast.error.url }}</dd> @if (toast.error) {
<dt class="col-sm-3 fw-normal text-end" i18n>Status</dt> <details>
<dd class="col-sm-9">{{ toast.error.status }} <em>{{ toast.error.statusText }}</em></dd> <div class="mt-2">
<dt class="col-sm-3 fw-normal text-end" i18n>Error</dt> @if (isDetailedError(toast.error)) {
<dd class="col-sm-9">{{ getErrorText(toast.error) }}</dd> <dl class="row mb-0">
</dl> <dt class="col-sm-3 fw-normal text-end">URL</dt>
<div class="row"> <dd class="col-sm-9">{{ toast.error.url }}</dd>
<div class="col offset-sm-3"> <dt class="col-sm-3 fw-normal text-end" i18n>Status</dt>
<button class="btn btn-sm btn-outline-dark" (click)="copyError(toast.error)"> <dd class="col-sm-9">{{ toast.error.status }} <em>{{ toast.error.statusText }}</em></dd>
<svg class="sidebaricon" fill="currentColor"> <dt class="col-sm-3 fw-normal text-end" i18n>Error</dt>
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard" /> <dd class="col-sm-9">{{ getErrorText(toast.error) }}</dd>
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check" /> </dl>
</svg>&nbsp;<ng-container i18n>Copy Raw Error</ng-container> }
</button> <div class="row">
</div> <div class="col offset-sm-3">
</div> <button class="btn btn-sm btn-outline-dark" (click)="copyError(toast.error)">
<svg class="sidebaricon" fill="currentColor">
@if (!copied) {
<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>
</button>
</div>
</div>
</div>
</details>
}
@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>
</details> <button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="toastService.closeToast(toast);"></button>
<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> </div>
</div> </ngb-toast>
<button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="toastService.closeToast(toast);"></button> }
</div>
</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

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

View File

@ -3,54 +3,63 @@
[title]="savedView.name" [title]="savedView.name"
[loading]="loading" [loading]="loading"
[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) {
<thead> <table content class="table table-hover mb-0 align-middle">
<tr> <thead>
<th scope="col" i18n>Created</th> <tr>
<th scope="col" i18n>Title</th> <th scope="col" i18n>Created</th>
<th scope="col" class="d-none d-md-table-cell" i18n>Tags</th> <th scope="col" i18n>Title</th>
<th scope="col" class="d-none d-md-table-cell" i18n>Correspondent</th> <th scope="col" class="d-none d-md-table-cell" i18n>Tags</th>
</tr> <th scope="col" class="d-none d-md-table-cell" i18n>Correspondent</th>
</thead> </tr>
<tbody> </thead>
<tr *ngFor="let doc of documents" (mouseleave)="maybeClosePopover()"> <tbody>
<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> @for (doc of documents; track doc) {
<td class="py-2 py-md-3"> <tr (mouseleave)="maybeClosePopover()">
<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 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> <td class="py-2 py-md-3">
<td class="py-2 py-md-3 d-none d-md-table-cell"> <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>
<pngx-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag> </td>
</td> <td class="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"> @for (t of doc.tags$ | async; track t) {
<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> <pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag>
<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" </td>
[ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle" <td class="position-relative py-2 py-md-3 d-none d-md-table-cell">
autoClose="true" popoverClass="shadow popover-preview" container="body" (mouseenter)="mouseEnterPreviewButton(doc)" (mouseleave)="mouseLeavePreviewButton()" #popover="ngbPopover"> @if (doc.correspondent !== null) {
<svg class="buttonicon-xs" fill="currentColor"> <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>
<use xlink:href="assets/bootstrap-icons.svg#eye"/> }
</svg> <div class="btn-group position-absolute top-50 end-0 translate-middle-y">
</a> <a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle"
<ng-template #previewContent> [ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle"
<pngx-preview-popup [document]="doc" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()"></pngx-preview-popup> autoClose="true" popoverClass="shadow popover-preview" container="body" (mouseenter)="mouseEnterPreviewButton(doc)" (mouseleave)="mouseLeavePreviewButton()" #popover="ngbPopover">
</ng-template> <svg class="buttonicon-xs" fill="currentColor">
<a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()"> <use xlink:href="assets/bootstrap-icons.svg#eye"/>
<svg class="buttonicon-xs" fill="currentColor"> </svg>
<use xlink:href="assets/bootstrap-icons.svg#download"/> </a>
</svg> <ng-template #previewContent>
</a> <pngx-preview-popup [document]="doc" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()"></pngx-preview-popup>
</div> </ng-template>
</td> <a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
</tr> <svg class="buttonicon-xs" fill="currentColor">
</tbody> <use xlink:href="assets/bootstrap-icons.svg#download"/>
</table> </svg>
</a>
<ng-template #empty> </div>
</td>
</tr>
}
</tbody>
</table>
} @else {
<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) {
<ng-container i18n>Documents in inbox</ng-container>: <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()">
<span class="badge rounded-pill" [class.bg-primary]="statistics?.documents_inbox > 0" [class.bg-muted]="statistics?.documents_inbox === 0">{{statistics?.documents_inbox}}</span> <ng-container i18n>Documents in inbox</ng-container>:
</a> <span class="badge rounded-pill" [class.bg-primary]="statistics?.documents_inbox > 0" [class.bg-muted]="statistics?.documents_inbox === 0">{{statistics?.documents_inbox}}</span>
</a>
}
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to documents" i18n-title routerLink="/documents/"> <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,60 +15,74 @@
<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="d-flex justify-content-between align-items-center my-2"> <div class="list-group-item filetypes">
<div class="progress flex-grow-1"> <div class="d-flex justify-content-between align-items-center my-2">
<div *ngFor="let filetype of statistics?.document_file_type_counts; let i = index; let last = last" <div class="progress flex-grow-1">
class="progress-bar bg-primary" @for (filetype of statistics?.document_file_type_counts; track filetype; let i = $index; let last = $last) {
role="progressbar" <div
[ngbPopover]="getFileTypeName(filetype)" class="progress-bar bg-primary"
i18n-ngbPopover role="progressbar"
triggers="mouseenter:mouseleave" [ngbPopover]="getFileTypeName(filetype)"
[attr.aria-label]="getFileTypeName(filetype)" i18n-ngbPopover
[class.me-1px]="!last" triggers="mouseenter:mouseleave"
[style.width]="getFileTypePercent(filetype) + '%'" [attr.aria-label]="getFileTypeName(filetype)"
[style.opacity]="getItemOpacity(i)" [class.me-1px]="!last"
[attr.aria-valuenow]="getFileTypePercent(filetype)" [style.width]="getFileTypePercent(filetype) + '%'"
aria-valuemin="0" [style.opacity]="getItemOpacity(i)"
aria-valuemax="100"> [attr.aria-valuenow]="getFileTypePercent(filetype)"
aria-valuemin="0"
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"> @for (filetype of statistics?.document_file_type_counts; track filetype; let i = $index) {
<div class="d-flex" *ngFor="let filetype of statistics?.document_file_type_counts; 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) {
<ng-container i18n>Tags</ng-container>: <a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/tags/">
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.tag_count | number}}</span> <ng-container i18n>Tags</ng-container>:
</a> <span class="badge bg-secondary text-light rounded-pill">{{statistics?.tag_count | number}}</span>
</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) {
<ng-container i18n>Correspondents</ng-container>: <a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/correspondents/">
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.correspondent_count | number}}</span> <ng-container i18n>Correspondents</ng-container>:
</a> <span class="badge bg-secondary text-light rounded-pill">{{statistics?.correspondent_count | number}}</span>
</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) {
<ng-container i18n>Document Types</ng-container>: <a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documenttypes/">
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.document_type_count | number}}</span> <ng-container i18n>Document Types</ng-container>:
</a> <span class="badge bg-secondary text-light rounded-pill">{{statistics?.document_type_count | number}}</span>
</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) {
<ng-container i18n>Storage Paths</ng-container>: <a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/storagepaths/">
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.storage_path_count | number}}</span> <ng-container i18n>Storage Paths</ng-container>:
</a> <span class="badge bg-secondary text-light rounded-pill">{{statistics?.storage_path_count | number}}</span>
</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>
<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"> @if (getStatusCompleted().length > 0) {
<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"/> <a class="btn-link" (click)="dismissCompleted()" [routerLink]="[]" >
<path d="M5.354 7.146l.896.897-.707.707-.897-.896a.5.5 0 1 1 .708-.708z"/> <span class="me-1" i18n="This button dismisses all status messages about processed documents on the dashboard (failed and successful)">Dismiss completed</span>
</svg> <svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-check2-all" viewBox="0 0 16 16">
</a> <path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7zm-4.208 7l-.896-.897.707-.707.543.543 6.646-6.647a.5.5 0 0 1 .708.708l-7 7a.5.5 0 0 1-.708 0z"/>
<path d="M5.354 7.146l.896.897-.707.707-.897-.896a.5.5 0 1 1 .708-.708z"/>
</svg>
</a>
}
</div> </div>
</div> </div>
<div *ngFor="let status of getStatus()"> @for (status of getStatus(); track status) {
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container> <div>
</div>
</div>
<div *ngIf="getStatusHidden().length" class="alerts-hidden">
<p *ngIf="!alertsExpanded" class="mt-3 mb-0 text-center">
<span i18n="This is shown as a summary line when there are more than 5 document in the processing pipeline.">{getStatusHidden().length, plural, =1 {One more document} other {{{getStatusHidden().length}} more documents}}</span>
&nbsp;&bull;&nbsp;
<a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a>
</p>
<div #hiddenAlerts="ngbCollapse" [(ngbCollapse)]="!alertsExpanded">
<div *ngFor="let status of getStatusHidden()">
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container> <ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
</div> </div>
</div> }
</div> </div>
@if (getStatusHidden().length) {
<div class="alerts-hidden">
@if (!alertsExpanded) {
<p class="mt-3 mb-0 text-center">
<span i18n="This is shown as a summary line when there are more than 5 document in the processing pipeline.">{getStatusHidden().length, plural, =1 {One more document} other {{{getStatusHidden().length}} more documents}}</span>
&nbsp;&bull;&nbsp;
<a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a>
</p>
}
<div #hiddenAlerts="ngbCollapse" [(ngbCollapse)]="!alertsExpanded">
@for (status of getStatusHidden(); track status) {
<div>
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
</div>
}
</div>
</div>
}
</div> </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>
<small i18n>Open document</small> @if (status.documentId) {
<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"> <button class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
<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"/> <small i18n>Open document</small>
</svg> <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">
</button> <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"/>
</div> </svg>
</button>
}
</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) {
<svg class="sidebaricon text-muted" fill="currentColor"> <div class="ms-n2 me-1" cdkDragHandle>
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/> <svg class="sidebaricon text-muted" fill="currentColor">
</svg> <use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/>
</div> </svg>
</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,43 +1,47 @@
<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" />
<div class="input-group-text" i18n>of {{previewNumPages}}</div> <div class="input-group-text" i18n>of {{previewNumPages}}</div>
</div> </div>
<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) {
{{ getZoomSettingTitle(setting) }} <option [value]="setting" [selected]="previewZoomSetting === setting">
</option> {{ getZoomSettingTitle(setting) }}
</select> </option>
<button class="btn btn-outline-secondary" (click)="increaseZoom()" i18n>+</button> }
</div> </select>
</ng-container> <button class="btn btn-outline-secondary" (click)="increaseZoom()" i18n>+</button>
</div>
}
<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">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span> </svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span>
</button> </button>
<div class="btn-group me-2"> <div class="btn-group me-2">
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary"> <a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
<svg class="buttonicon me-md-1" fill="currentColor"> <svg class="buttonicon me-md-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#download" /> <use xlink:href="assets/bootstrap-icons.svg#download" />
</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>
<button class="btn btn-sm btn-outline-primary me-2" id="actionsDropdown" ngbDropdownToggle> <button class="btn btn-sm btn-outline-primary me-2" id="actionsDropdown" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor"> <svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#three-dots" /> <use xlink:href="assets/bootstrap-icons.svg#three-dots" />
@ -46,240 +50,287 @@
</button> </button>
<div ngbDropdownMenu aria-labelledby="actionsDropdown" class="shadow"> <div ngbDropdownMenu aria-labelledby="actionsDropdown" class="shadow">
<button ngbDropdownItem (click)="redoOcr()" [disabled]="!userCanEdit"> <button ngbDropdownItem (click)="redoOcr()" [disabled]="!userCanEdit">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" /> <use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" />
</svg><span class="ps-1" i18n>Redo OCR</span> </svg><span class="ps-1" i18n>Redo OCR</span>
</button> </button>
<button ngbDropdownItem (click)="moreLike()"> <button ngbDropdownItem (click)="moreLike()">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#diagram-3" /> <use xlink:href="assets/bootstrap-icons.svg#diagram-3" />
</svg><span class="ps-1" i18n>More like this</span> </svg><span class="ps-1" i18n>More like this</span>
</button> </button>
</div> </div>
</div> </div>
<pngx-custom-fields-dropdown <pngx-custom-fields-dropdown
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }"
class="me-2" class="me-2"
[documentId]="documentId" [documentId]="documentId"
[disabled]="!userIsOwner" [disabled]="!userIsOwner"
[existingFields]="document?.custom_fields" [existingFields]="document?.custom_fields"
(created)="refreshCustomFields()" (created)="refreshCustomFields()"
(added)="addField($event)"> (added)="addField($event)">
</pngx-custom-fields-dropdown> </pngx-custom-fields-dropdown>
<pngx-share-links-dropdown [documentId]="documentId" [hasArchiveVersion]="!!document?.archived_file_name" [disabled]="!userIsOwner" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.ShareLink }"></pngx-share-links-dropdown> <pngx-share-links-dropdown [documentId]="documentId" [hasArchiveVersion]="!!document?.archived_file_name" [disabled]="!userIsOwner" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.ShareLink }"></pngx-share-links-dropdown>
</pngx-page-header> </pngx-page-header>
<div class="row"> <div class="row">
<div class="col-md-6 col-xl-4 mb-4"> <div class="col-md-6 col-xl-4 mb-4">
<form [formGroup]='documentForm' (ngSubmit)="save()"> <form [formGroup]='documentForm' (ngSubmit)="save()">
<div class="btn-toolbar mb-1 pb-3 border-bottom"> <div class="btn-toolbar mb-1 pb-3 border-bottom">
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Close" (click)="close()"> <button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Close" (click)="close()">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x" /> <use xlink:href="assets/bootstrap-icons.svg#x" />
</svg> </svg>
</button> </button>
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Previous" (click)="previousDoc()" [disabled]="!hasPrevious()"> <button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Previous" (click)="previousDoc()" [disabled]="!hasPrevious()">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-left" /> <use xlink:href="assets/bootstrap-icons.svg#arrow-left" />
</svg> </svg>
</button> </button>
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Next" (click)="nextDoc()" [disabled]="!hasNext()"> <button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Next" (click)="nextDoc()" [disabled]="!hasNext()">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" 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> </div>
<ng-container *ngTemplateOutlet="saveButtons"></ng-container> <ng-container *ngTemplateOutlet="saveButtons"></ng-container>
</div> </div>
<ul ngbNav #nav="ngbNav" class="nav-underline flex-nowrap flex-md-wrap overflow-auto" (navChange)="onNavChange($event)" [(activeId)]="activeNavID"> <ul ngbNav #nav="ngbNav" class="nav-underline flex-nowrap flex-md-wrap overflow-auto" (navChange)="onNavChange($event)" [(activeId)]="activeNavID">
<li [ngbNavItem]="DocumentDetailNavIDs.Details"> <li [ngbNavItem]="DocumentDetailNavIDs.Details">
<a ngbNavLink i18n>Details</a> <a ngbNavLink i18n>Details</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<div> <div>
<pngx-input-text #inputTitle i18n-title title="Title" formControlName="title" [horizontal]="true" (keyup)="titleKeyUp($event)" [error]="error?.title"></pngx-input-text> <pngx-input-text #inputTitle i18n-title title="Title" formControlName="title" [horizontal]="true" (keyup)="titleKeyUp($event)" [error]="error?.title"></pngx-input-text>
<pngx-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" [horizontal]="true" formControlName='archive_serial_number'></pngx-input-number> <pngx-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" [horizontal]="true" formControlName='archive_serial_number'></pngx-input-number>
<pngx-input-date i18n-title title="Date created" formControlName="created_date" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" <pngx-input-date i18n-title title="Date created" formControlName="created_date" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
[error]="error?.created_date"></pngx-input-date> [error]="error?.created_date"></pngx-input-date>
<pngx-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" <pngx-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
(createNew)="createCorrespondent($event)" [suggestions]="suggestions?.correspondents" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"></pngx-input-select> (createNew)="createCorrespondent($event)" [suggestions]="suggestions?.correspondents" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"></pngx-input-select>
<pngx-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" <pngx-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
(createNew)="createDocumentType($event)" [suggestions]="suggestions?.document_types" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"></pngx-input-select> (createNew)="createDocumentType($event)" [suggestions]="suggestions?.document_types" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"></pngx-input-select>
<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) {
</div> <pngx-input-number formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [error]="getCustomFieldError(i)"></pngx-input-number>
</ng-container> }
@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>
}
</div>
<div class="d-flex border-top pt-3"> <div class="d-flex border-top pt-3">
<ng-container *ngTemplateOutlet="saveButtons"></ng-container> <ng-container *ngTemplateOutlet="saveButtons"></ng-container>
</div> </div>
</ng-template> </ng-template>
</li> </li>
<li [ngbNavItem]="DocumentDetailNavIDs.Content"> <li [ngbNavItem]="DocumentDetailNavIDs.Content">
<a ngbNavLink i18n>Content</a> <a ngbNavLink i18n>Content</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<div> <div>
<textarea class="form-control" id="content" rows="20" formControlName='content' [class.rtl]="isRTL"></textarea> <textarea class="form-control" id="content" rows="20" formControlName='content' [class.rtl]="isRTL"></textarea>
</div> </div>
</ng-template> </ng-template>
</li> </li>
<li [ngbNavItem]="DocumentDetailNavIDs.Metadata"> <li [ngbNavItem]="DocumentDetailNavIDs.Metadata">
<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) {
<tbody> <table class="table table-borderless">
<tr> <tbody>
<td i18n>Date modified</td> <tr>
<td>{{document.modified | customDate}}</td> <td i18n>Date modified</td>
</tr> <td>{{document.modified | customDate}}</td>
<tr> </tr>
<td i18n>Date added</td> <tr>
<td>{{document.added | customDate}}</td> <td i18n>Date added</td>
</tr> <td>{{document.added | customDate}}</td>
<tr> </tr>
<td i18n>Media filename</td> <tr>
<td>{{metadata?.media_filename}}</td> <td i18n>Media filename</td>
</tr> <td>{{metadata?.media_filename}}</td>
<tr> </tr>
<td i18n>Original filename</td> <tr>
<td>{{metadata?.original_filename}}</td> <td i18n>Original filename</td>
</tr> <td>{{metadata?.original_filename}}</td>
<tr> </tr>
<td i18n>Original MD5 checksum</td> <tr>
<td>{{metadata?.original_checksum}}</td> <td i18n>Original MD5 checksum</td>
</tr> <td>{{metadata?.original_checksum}}</td>
<tr> </tr>
<td i18n>Original file size</td> <tr>
<td>{{metadata?.original_size | fileSize}}</td> <td i18n>Original file size</td>
</tr> <td>{{metadata?.original_size | fileSize}}</td>
<tr> </tr>
<td i18n>Original mime type</td> <tr>
<td>{{metadata?.original_mime_type}}</td> <td i18n>Original mime type</td>
</tr> <td>{{metadata?.original_mime_type}}</td>
<tr *ngIf="metadata?.has_archive_version"> </tr>
<td i18n>Archive MD5 checksum</td> @if (metadata?.has_archive_version) {
<td>{{metadata?.archive_checksum}}</td> <tr>
</tr> <td i18n>Archive MD5 checksum</td>
<tr *ngIf="metadata?.has_archive_version"> <td>{{metadata?.archive_checksum}}</td>
<td i18n>Archive file size</td> </tr>
<td>{{metadata?.archive_size | fileSize}}</td> }
</tr> @if (metadata?.has_archive_version) {
</tbody> <tr>
</table> <td i18n>Archive file size</td>
<td>{{metadata?.archive_size | fileSize}}</td>
</tr>
}
</tbody>
</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-container *ngTemplateOutlet="previewContent"></ng-container>
</ng-template>
</li>
<li [ngbNavItem]="DocumentDetailNavIDs.Notes" *ngIf="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>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<pngx-document-notes [documentId]="documentId" [notes]="document?.notes" [addDisabled]="!userCanEdit" (updated)="notesUpdated($event)"></pngx-document-notes> <ng-container *ngTemplateOutlet="previewContent"></ng-container>
</ng-template> </ng-template>
}
</li> </li>
<li [ngbNavItem]="DocumentDetailNavIDs.Permissions" *ngIf="showPermissions"> @if (notesEnabled) {
<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>
<pngx-document-notes [documentId]="documentId" [notes]="document?.notes" [addDisabled]="!userCanEdit" (updated)="notesUpdated($event)"></pngx-document-notes>
</ng-template>
</li>
}
@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">
<pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form> <pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form>
</div> </div>
</ng-template> </ng-template>
</li> </li>
</ul> }
</ul>
<div [ngbNavOutlet]="nav" class="mt-3"></div> <div [ngbNavOutlet]="nav" class="mt-3"></div>
</form>
</div>
<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 *ngIf="renderAsPlainText">
<div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
</ng-container>
<div *ngIf="requiresPassword" class="password-prompt">
<form>
<input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
</form> </form>
</div>
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview>
<ng-container *ngTemplateOutlet="previewContent"></ng-container>
@if (renderAsPlainText) {
<div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
}
@if (requiresPassword) {
<div class="password-prompt">
<form>
<input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
</form>
</div>
}
</div>
</div> </div>
</div>
</div> <ng-template #saveButtons>
<div class="btn-group ms-auto">
<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>
@if (hasNext()) {
<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>
<button type="button" class="order-0 btn btn-sm btn-outline-secondary" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button>
</div>
</ng-template>
<ng-template #saveButtons> <ng-template #previewContent>
<div class="btn-group ms-auto"> @if (!metadata) {
<ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"> <div class="w-100 h-100 d-flex align-items-center justify-content-center">
<button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button> <div>
<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> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<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> <ng-container i18n>Loading...</ng-container>
</ng-container> </div>
<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> @if (getContentType() === 'application/pdf') {
@if (!useNativePdfViewer ) {
<ng-template #previewContent> <div class="preview-sticky pdf-viewer-container">
<div *ngIf="!metadata" class="w-100 h-100 d-flex align-items-center justify-content-center"> <pngx-pdf-viewer
<div> [src]="{ url: previewUrl, password: password }"
<div class="spinner-border spinner-border-sm me-2" role="status"></div> [original-size]="false"
<ng-container i18n>Loading...</ng-container> [show-borders]="true"
</div> [show-all]="true"
</div> [(page)]="previewCurrentPage"
<ng-container *ngIf="getContentType() === 'application/pdf'"> [zoom-scale]="previewZoomScale"
<div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer"> [zoom]="previewZoomSetting"
<pngx-pdf-viewer (error)="onError($event)"
[src]="{ url: previewUrl, password: password }" (after-load-complete)="pdfPreviewLoaded($event)">
[original-size]="false" </pngx-pdf-viewer>
[show-borders]="true" </div>
[show-all]="true" } @else {
[(page)]="previewCurrentPage" <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
[zoom-scale]="previewZoomScale" }
[zoom]="previewZoomSetting" }
(error)="onError($event)" @if (renderAsPlainText) {
(after-load-complete)="pdfPreviewLoaded($event)"> <div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
</pngx-pdf-viewer> }
</div> @if (showPasswordField) {
<ng-template #nativePdfViewer> <div class="password-prompt">
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object> <form>
</ng-template> <input autocomplete="" autofocus="true" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
</ng-container> </form>
<ng-container *ngIf="renderAsPlainText"> </div>
<div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div> }
</ng-container> </ng-template>
<div *ngIf="showPasswordField" class="password-prompt">
<form>
<input autocomplete="" autofocus="true" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
</form>
</div>
</ng-template>

View File

@ -1,23 +1,29 @@
<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) {
<use xlink:href="assets/bootstrap-icons.svg#caret-down" /> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#caret-down" />
</svg> </svg>
<svg class="buttonicon" fill="currentColor" *ngIf="expand"> }
<use xlink:href="assets/bootstrap-icons.svg#caret-up" /> @if (expand) {
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#caret-up" />
</svg> </svg>
}
</button> </button>
{{title}} {{title}}
</h6> </h6>
<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) {
<td>{{m.prefix}}:{{m.key}}</td> <tr>
<td class="metadata-column">{{m.value}}</td> <td>{{m.prefix}}:{{m.key}}</td>
</tr> <td class="metadata-column">{{m.value}}</td>
</tbody> </tr>
}
</tbody>
</table> </table>
</div> </div>

View File

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

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>
<svg width="1em" height="1em" fill="currentColor" class="me-2"> }
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> @for (highlight of searchNoteHighlights; track highlight) {
</svg> <span class="d-block">
<span [innerHtml]="highlight"></span> <svg width="1em" height="1em" fill="currentColor" class="me-2">
</span> <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
<span *ngIf="!document.__search_hit__" class="result-content">{{contentTrimmed}}</span> </svg>
<span [innerHtml]="highlight"></span>
</span>
}
@if (!document.__search_hit__) {
<span class="result-content">{{contentTrimmed}}</span>
}
</p> </p>
@ -41,91 +53,105 @@
<a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()"> <a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()">
<svg class="sidebaricon" fill="currentColor" class="sidebaricon"> <svg class="sidebaricon" fill="currentColor" class="sidebaricon">
<use xlink:href="assets/bootstrap-icons.svg#diagram-3"/> <use xlink:href="assets/bootstrap-icons.svg#diagram-3"/>
</svg>&nbsp;<span class="d-none d-md-inline" i18n>More like this</span> </svg>&nbsp;<span class="d-none d-md-inline" i18n>More like this</span>
</a> </a>
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"> <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<svg class="sidebaricon" fill="currentColor" class="sidebaricon"> <svg class="sidebaricon" fill="currentColor" class="sidebaricon">
<use xlink:href="assets/bootstrap-icons.svg#pencil"/> <use xlink:href="assets/bootstrap-icons.svg#pencil"/>
</svg>&nbsp;<span class="d-none d-md-inline" i18n>Edit</span> </svg>&nbsp;<span class="d-none d-md-inline" i18n>Edit</span>
</a> </a>
<a class="btn btn-sm btn-outline-secondary" target="_blank" [href]="previewUrl" <a class="btn btn-sm btn-outline-secondary" target="_blank" [href]="previewUrl"
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
<svg class="sidebaricon" fill="currentColor" class="sidebaricon"> <svg class="sidebaricon" fill="currentColor" class="sidebaricon">
<use xlink:href="assets/bootstrap-icons.svg#eye"/> <use xlink:href="assets/bootstrap-icons.svg#eye"/>
</svg>&nbsp;<span class="d-none d-md-inline" i18n>View</span> </svg>&nbsp;<span class="d-none d-md-inline" i18n>View</span>
</a> </a>
<ng-template #previewContent> <ng-template #previewContent>
<pngx-preview-popup [document]="document"></pngx-preview-popup> <pngx-preview-popup [document]="document"></pngx-preview-popup>
</ng-template> </ng-template>
<a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()"> <a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
<svg class="sidebaricon" fill="currentColor" class="sidebaricon"> <svg class="sidebaricon" fill="currentColor" class="sidebaricon">
<use xlink:href="assets/bootstrap-icons.svg#download"/> <use xlink:href="assets/bootstrap-icons.svg#download"/>
</svg>&nbsp;<span class="d-none d-md-inline" i18n>Download</span> </svg>&nbsp;<span class="d-none d-md-inline" i18n>Download</span>
</a> </a>
</div> </div>
<div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0"> <div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
<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) {
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> <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>
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
</svg> <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
<small i18n>{{document.notes.length}} Notes</small> </svg>
</button> <small i18n>{{document.notes.length}} Notes</small>
<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 </button>
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> }
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> @if (document.document_type) {
<use xlink:href="assets/bootstrap-icons.svg#file-earmark"/> <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
</svg> (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
<small>{{(document.document_type$ | async)?.name}}</small> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
</button> <use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
<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 </svg>
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()"> <small>{{(document.document_type$ | async)?.name}}</small>
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> </button>
<use xlink:href="assets/bootstrap-icons.svg#archive"/> }
</svg> @if (document.storage_path) {
<small>{{(document.storage_path$ | async)?.name}}</small> <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
</button> (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
<div *ngIf="document.archive_serial_number | isNumber" class="list-group-item me-2 bg-light text-dark p-1 border-0"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<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#upc-scan"/> </svg>
</svg> <small>{{(document.storage_path$ | async)?.name}}</small>
<small>#{{document.archive_serial_number}}</small> </button>
</div> }
<ng-template #dateTooltip> @if (document.archive_serial_number | isNumber) {
<div class="d-flex flex-column text-light"> <div class="list-group-item me-2 bg-light text-dark p-1 border-0">
<span i18n>Created: {{ document.created | customDate }}</span> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<span i18n>Added: {{ document.added | customDate }}</span> <use xlink:href="assets/bootstrap-icons.svg#upc-scan"/>
<span i18n>Modified: {{ document.modified | customDate }}</span> </svg>
<small>#{{document.archive_serial_number}}</small>
</div>
}
<ng-template #dateTooltip>
<div class="d-flex flex-column text-light">
<span i18n>Created: {{ document.created | customDate }}</span>
<span i18n>Added: {{ document.added | customDate }}</span>
<span i18n>Modified: {{ document.modified | customDate }}</span>
</div>
</ng-template>
<div class="list-group-item bg-light text-dark p-1 border-0" [ngbTooltip]="dateTooltip">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#calendar-event"/>
</svg>
<small>{{document.created_date | customDate:'mediumDate'}}</small>
</div>
@if (document.owner && document.owner !== settingsService.currentUser.id) {
<div class="list-group-item bg-light text-dark p-1 border-0">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
</svg>
<small>{{document.owner | username}}</small>
</div>
}
@if (document.is_shared_by_requester) {
<div class="list-group-item bg-light text-dark p-1 border-0">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
</svg>
<small i18n>Shared</small>
</div>
}
@if (document.__search_hit__?.score) {
<div class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score">
<small class="text-muted" i18n>Score:</small>
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
</div>
}
</div>
</div>
</div> </div>
</ng-template>
<div class="list-group-item bg-light text-dark p-1 border-0" [ngbTooltip]="dateTooltip">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#calendar-event"/>
</svg>
<small>{{document.created_date | customDate:'mediumDate'}}</small>
</div>
<div *ngIf="document.owner && document.owner !== settingsService.currentUser.id" class="list-group-item bg-light text-dark p-1 border-0">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
</svg>
<small>{{document.owner | username}}</small>
</div>
<div *ngIf="document.is_shared_by_requester" class="list-group-item bg-light text-dark p-1 border-0">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
</svg>
<small i18n>Shared</small>
</div>
<div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score">
<small class="text-muted" i18n>Score:</small>
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
</div> </div>
</div> </div>
</div> </div>
</div>
</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>
<span class="badge text-dark">+ {{moreTags}}</span> }
</div> @if (moreTags) {
<div>
<span class="badge text-dark">+ {{moreTags}}</span>
</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) {
<span class="badge rounded-pill bg-light border text-primary"> <a routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1">
<svg class="metadata-icon ms-1 me-1" fill="currentColor"> <span class="badge rounded-pill bg-light border text-primary">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> <svg class="metadata-icon ms-1 me-1" fill="currentColor">
</svg> <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
{{document.notes.length}}</span> </svg>
</a> {{document.notes.length}}</span>
</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) {
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> <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
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark"/> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
</svg> <use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
<small>{{(document.document_type$ | async)?.name ?? privateName}}</small> </svg>
</button> <small>{{(document.document_type$ | async)?.name ?? privateName}}</small>
<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 </button>
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()"> }
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> @if (document.storage_path) {
<use xlink:href="assets/bootstrap-icons.svg#folder"/> <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
</svg> (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
</button> <use xlink:href="assets/bootstrap-icons.svg#folder"/>
</svg>
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
</button>
}
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between"> <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) {
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> <div class="ps-0 p-1">
<use xlink:href="assets/bootstrap-icons.svg#upc-scan"/> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
</svg> <use xlink:href="assets/bootstrap-icons.svg#upc-scan"/>
<small>#{{document.archive_serial_number}}</small> </svg>
</div> <small>#{{document.archive_serial_number}}</small>
<div *ngIf="document.owner && document.owner !== settingsService.currentUser.id" class="ps-0 p-1"> </div>
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> }
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/> @if (document.owner && document.owner !== settingsService.currentUser.id) {
</svg> <div class="ps-0 p-1">
<small>{{document.owner | username}}</small> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
</div> <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
<div *ngIf="document.is_shared_by_requester" class="ps-0 p-1"> </svg>
<svg class="metadata-icon me-2 text-muted" fill="currentColor"> <small>{{document.owner | username}}</small>
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/> </div>
</svg> }
<small i18n>Shared</small> @if (document.is_shared_by_requester) {
</div> <div class="ps-0 p-1">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
</svg>
<small i18n>Shared</small>
</div>
}
</div> </div>
<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">
@ -92,8 +108,8 @@
</svg> </svg>
</a> </a>
<a [href]="previewUrl" target="_blank" class="btn btn-sm btn-outline-secondary" <a [href]="previewUrl" target="_blank" class="btn btn-sm btn-outline-secondary"
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/> <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/> <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>

View File

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

View File

@ -5,17 +5,25 @@
<div ngbDropdown> <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()">
</select> @for (m of textFilterModifiers; track m) {
<button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()"> <option ngbDropdownItem [value]="m.id">{{m.label}}</option>
<svg fill="currentColor" class="buttonicon-sm me-1"> }
<use xlink:href="assets/bootstrap-icons.svg#x"/> </select>
</svg> }
</button> @if (_textFilter) {
<button class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()">
<svg fill="currentColor" class="buttonicon-sm me-1">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
</button>
}
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget === 'fulltext-morelike'"> <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>
@ -31,7 +39,7 @@
(selectionModelChange)="updateRules()" (selectionModelChange)="updateRules()"
(opened)="onTagsDropdownOpen()" (opened)="onTagsDropdownOpen()"
[documentCounts]="tagDocumentCounts" [documentCounts]="tagDocumentCounts"
[allowSelectNone]="true"></pngx-filterable-dropdown> [allowSelectNone]="true"></pngx-filterable-dropdown>
<pngx-filterable-dropdown class="flex-fill" title="Correspondent" icon="person-fill" i18n-title <pngx-filterable-dropdown class="flex-fill" title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[items]="correspondents" [items]="correspondents"
@ -39,7 +47,7 @@
(selectionModelChange)="updateRules()" (selectionModelChange)="updateRules()"
(opened)="onCorrespondentDropdownOpen()" (opened)="onCorrespondentDropdownOpen()"
[documentCounts]="correspondentDocumentCounts" [documentCounts]="correspondentDocumentCounts"
[allowSelectNone]="true"></pngx-filterable-dropdown> [allowSelectNone]="true"></pngx-filterable-dropdown>
<pngx-filterable-dropdown class="flex-fill" title="Document type" icon="file-earmark-fill" i18n-title <pngx-filterable-dropdown class="flex-fill" title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder filterPlaceholder="Filter document types" i18n-filterPlaceholder
[items]="documentTypes" [items]="documentTypes"
@ -47,7 +55,7 @@
(selectionModelChange)="updateRules()" (selectionModelChange)="updateRules()"
(opened)="onDocumentTypeDropdownOpen()" (opened)="onDocumentTypeDropdownOpen()"
[documentCounts]="documentTypeDocumentCounts" [documentCounts]="documentTypeDocumentCounts"
[allowSelectNone]="true"></pngx-filterable-dropdown> [allowSelectNone]="true"></pngx-filterable-dropdown>
<pngx-filterable-dropdown class="flex-fill" title="Storage path" icon="folder-fill" i18n-title <pngx-filterable-dropdown class="flex-fill" title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[items]="storagePaths" [items]="storagePaths"
@ -55,7 +63,7 @@
(selectionModelChange)="updateRules()" (selectionModelChange)="updateRules()"
(opened)="onStoragePathDropdownOpen()" (opened)="onStoragePathDropdownOpen()"
[documentCounts]="storagePathDocumentCounts" [documentCounts]="storagePathDocumentCounts"
[allowSelectNone]="true"></pngx-filterable-dropdown> [allowSelectNone]="true"></pngx-filterable-dropdown>
</div> </div>
<div class="d-flex flex-wrap gap-2"> <div class="d-flex flex-wrap gap-2">
<pngx-date-dropdown <pngx-date-dropdown
@ -63,27 +71,27 @@
(datesSet)="updateRules()" (datesSet)="updateRules()"
[(dateBefore)]="dateCreatedBefore" [(dateBefore)]="dateCreatedBefore"
[(dateAfter)]="dateCreatedAfter" [(dateAfter)]="dateCreatedAfter"
[(relativeDate)]="dateCreatedRelativeDate"></pngx-date-dropdown> [(relativeDate)]="dateCreatedRelativeDate"></pngx-date-dropdown>
<pngx-date-dropdown <pngx-date-dropdown
title="Added" i18n-title title="Added" i18n-title
(datesSet)="updateRules()" (datesSet)="updateRules()"
[(dateBefore)]="dateAddedBefore" [(dateBefore)]="dateAddedBefore"
[(dateAfter)]="dateAddedAfter" [(dateAfter)]="dateAddedAfter"
[(relativeDate)]="dateAddedRelativeDate"></pngx-date-dropdown> [(relativeDate)]="dateAddedRelativeDate"></pngx-date-dropdown>
</div> </div>
<div class="d-flex flex-wrap"> <div class="d-flex flex-wrap">
<pngx-permissions-filter-dropdown <pngx-permissions-filter-dropdown
title="Permissions" i18n-title title="Permissions" i18n-title
(ownerFilterSet)="updateRules()" (ownerFilterSet)="updateRules()"
[(selectionModel)]="permissionsSelectionModel"></pngx-permissions-filter-dropdown> [(selectionModel)]="permissionsSelectionModel"></pngx-permissions-filter-dropdown>
</div> </div>
<div class="d-flex flex-wrap d-none d-sm-inline-block"> <div class="d-flex flex-wrap d-none d-sm-inline-block">
<button class="btn btn-outline-secondary btn-sm" [disabled]="!rulesModified" (click)="resetSelected()"> <button class="btn btn-outline-secondary btn-sm" [disabled]="!rulesModified" (click)="resetSelected()">
<svg class="toolbaricon ms-n1" fill="currentColor"> <svg class="toolbaricon ms-n1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"></use> <use xlink:href="assets/bootstrap-icons.svg#x"></use>
</svg><ng-container i18n>Reset filters</ng-container> </svg><ng-container i18n>Reset filters</ng-container>
</button> </button>
</div>
</div> </div>
</div> </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) {
<h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6> <div class="alert alert-danger" role="alert">
<ng-container i18n>The error returned was</ng-container>:<br/> <h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6>
{{ error.filter_rules }} <ng-container i18n>The error returned was</ng-container>:<br/>
</div> {{ error.filter_rules }}
</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,29 +1,35 @@
<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>
<div class="invalid-feedback" i18n> <div class="invalid-feedback" i18n>
Please enter a note. Please enter a note.
</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) {
<button type="button" class="btn btn-primary btn-sm" [disabled]="networkActive || addDisabled" (click)="addNote()" i18n>Add note</button> <div class="spinner-border spinner-border-sm fw-normal me-auto" role="status"></div>
</div> }
<button type="button" class="btn btn-primary btn-sm" [disabled]="networkActive || addDisabled" (click)="addNote()" i18n>Add note</button>
</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-body text-dark"> <div class="card border mb-3">
<div class="card-body text-dark">
<p class="card-text">{{note.note}}</p> <p class="card-text">{{note.note}}</p>
</div> </div>
<div class="d-flex card-footer small bg-light text-primary justify-content-between align-items-center"> <div class="d-flex card-footer small bg-light text-primary justify-content-between align-items-center">
<span>{{displayName(note)}} - {{ note.created | customDate}}</span> <span>{{displayName(note)}} - {{ note.created | customDate}}</span>
<button type="button" class="btn btn-link btn-sm p-0 fade" title="Delete note" i18n-title (click)="deleteNote(note.id)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Note }"> <button type="button" class="btn btn-link btn-sm p-0 fade" title="Delete note" i18n-title (click)="deleteNote(note.id)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Note }">
<svg width="13" height="13" fill="currentColor"> <svg width="13" height="13" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg> </svg>
<span class="visually-hidden" i18n>Delete note</span> <span class="visually-hidden" i18n>Delete note</span>
</button> </button>
</div>
</div> </div>
}
</div> </div>
</div> }

View File

@ -1,43 +1,47 @@
<pngx-page-header title="Consumption Templates" i18n-title> <pngx-page-header title="Consumption Templates" i18n-title>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editTemplate()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.ConsumptionTemplate }"> <button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editTemplate()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.ConsumptionTemplate }">
<svg class="sidebaricon me-1" fill="currentColor"> <svg class="sidebaricon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> <use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg> </svg>
<ng-container i18n>Add Template</ng-container> <ng-container i18n>Add Template</ng-container>
</button> </button>
</pngx-page-header> </pngx-page-header>
<ul class="list-group"> <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>
<div class="col" i18n>Sort order</div> <div class="col" i18n>Sort order</div>
<div class="col" i18n>Document Sources</div> <div class="col" i18n>Document Sources</div>
<div class="col" i18n>Actions</div> <div class="col" i18n>Actions</div>
</div> </div>
</li> </li>
<li *ngFor="let template of templates" class="list-group-item"> @for (template of templates; track template) {
<div class="row"> <li class="list-group-item">
<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="row">
<div class="col d-flex align-items-center"><code>{{template.order}}</code></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">{{getSourceList(template)}}</div> <div class="col d-flex align-items-center"><code>{{template.order}}</code></div>
<div class="col"> <div class="col d-flex align-items-center">{{getSourceList(template)}}</div>
<div class="btn-group"> <div class="col">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editTemplate(template)"> <div class="btn-group">
<svg class="buttonicon-sm" fill="currentColor"> <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editTemplate(template)">
<use xlink:href="assets/bootstrap-icons.svg#pencil" /> <svg class="buttonicon-sm" fill="currentColor">
</svg>&nbsp;<ng-container i18n>Edit</ng-container> <use xlink:href="assets/bootstrap-icons.svg#pencil" />
</button> </svg>&nbsp;<ng-container i18n>Edit</ng-container>
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteTemplate(template)"> </button>
<svg class="buttonicon-sm" fill="currentColor"> <button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteTemplate(template)">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <svg class="buttonicon-sm" fill="currentColor">
</svg>&nbsp;<ng-container i18n>Delete</ng-container> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</button> </svg>&nbsp;<ng-container i18n>Delete</ng-container>
</div> </button>
</div>
</div> </div>
</div> </div>
</li> </li>
<li *ngIf="templates.length === 0" class="list-group-item" i18n>No templates defined.</li> }
</ul> @if (templates.length === 0) {
<li class="list-group-item" i18n>No templates defined.</li>
}
</ul>

View File

@ -1,41 +1,45 @@
<pngx-page-header title="Custom Fields" i18n-title> <pngx-page-header title="Custom Fields" i18n-title>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editField()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }"> <button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editField()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
<svg class="sidebaricon me-1" fill="currentColor"> <svg class="sidebaricon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> <use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg> </svg>
<ng-container i18n>Add Field</ng-container> <ng-container i18n>Add Field</ng-container>
</button> </button>
</pngx-page-header> </pngx-page-header>
<ul class="list-group"> <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>
<div class="col" i18n>Data Type</div> <div class="col" i18n>Data Type</div>
<div class="col" i18n>Actions</div> <div class="col" i18n>Actions</div>
</div> </div>
</li> </li>
<li *ngFor="let field of fields" class="list-group-item"> @for (field of fields; track field) {
<div class="row"> <li class="list-group-item">
<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="row">
<div class="col d-flex align-items-center">{{getDataType(field)}}</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"> <div class="col d-flex align-items-center">{{getDataType(field)}}</div>
<div class="btn-group"> <div class="col">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editField(field)"> <div class="btn-group">
<svg class="buttonicon-sm" fill="currentColor"> <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editField(field)">
<use xlink:href="assets/bootstrap-icons.svg#pencil" /> <svg class="buttonicon-sm" fill="currentColor">
</svg>&nbsp;<ng-container i18n>Edit</ng-container> <use xlink:href="assets/bootstrap-icons.svg#pencil" />
</button> </svg>&nbsp;<ng-container i18n>Edit</ng-container>
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteField(field)"> </button>
<svg class="buttonicon-sm" fill="currentColor"> <button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteField(field)">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <svg class="buttonicon-sm" fill="currentColor">
</svg>&nbsp;<ng-container i18n>Delete</ng-container> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</button> </svg>&nbsp;<ng-container i18n>Delete</ng-container>
</div> </button>
</div>
</div> </div>
</div> </div>
</li> </li>
<li *ngIf="fields.length === 0" class="list-group-item" i18n>No fields defined.</li> }
</ul> @if (fields.length === 0) {
<li class="list-group-item" i18n>No fields defined.</li>
}
</ul>

View File

@ -2,104 +2,114 @@
</pngx-page-header> </pngx-page-header>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }">
<h4> <h4>
<ng-container i18n>Mail accounts</ng-container> <ng-container i18n>Mail accounts</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editMailAccount()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailAccount }"> <button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editMailAccount()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailAccount }">
<svg class="sidebaricon me-1" fill="currentColor"> <svg class="sidebaricon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> <use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg> </svg>
<ng-container i18n>Add Account</ng-container> <ng-container i18n>Add Account</ng-container>
</button> </button>
</h4> </h4>
<ul class="list-group"> <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>
<div class="col" i18n>Server</div> <div class="col" i18n>Server</div>
<div class="col" i18n>Actions</div> <div class="col" i18n>Actions</div>
</div> </div>
</li> </li>
<li *ngFor="let account of mailAccounts" class="list-group-item"> @for (account of mailAccounts; track account) {
<div class="row"> <li class="list-group-item">
<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="row">
<div class="col d-flex align-items-center">{{account.imap_server}}</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"> <div class="col d-flex align-items-center">{{account.imap_server}}</div>
<div class="btn-group"> <div class="col">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" [disabled]="!userCanEdit(account)" class="btn btn-sm btn-outline-secondary" type="button" (click)="editMailAccount(account)"> <div class="btn-group">
<svg class="buttonicon-sm" fill="currentColor"> <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" [disabled]="!userCanEdit(account)" class="btn btn-sm btn-outline-secondary" type="button" (click)="editMailAccount(account)">
<use xlink:href="assets/bootstrap-icons.svg#pencil" /> <svg class="buttonicon-sm" fill="currentColor">
</svg>&nbsp;<ng-container i18n>Edit</ng-container> <use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button> </button>
<button *pngxIfOwner="account" class="btn btn-sm btn-outline-secondary" type="button" (click)="editPermissions(account)"> <button *pngxIfOwner="account" class="btn btn-sm btn-outline-secondary" type="button" (click)="editPermissions(account)">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-lock" /> <use xlink:href="assets/bootstrap-icons.svg#person-lock" />
</svg>&nbsp;<ng-container i18n>Permissions</ng-container> </svg>&nbsp;<ng-container i18n>Permissions</ng-container>
</button> </button>
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailAccount }" [disabled]="!userIsOwner(account)" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailAccount(account)"> <button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailAccount }" [disabled]="!userIsOwner(account)" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailAccount(account)">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container> </svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button> </button>
</div>
</div> </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) {
</ul> <li class="list-group-item" i18n>No mail accounts defined.</li>
}
</ul>
</ng-container> </ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }"> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }">
<h4 class="mt-4"> <h4 class="mt-4">
<ng-container i18n>Mail rules</ng-container> <ng-container i18n>Mail rules</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editMailRule()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailRule }"> <button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editMailRule()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailRule }">
<svg class="sidebaricon me-1" fill="currentColor"> <svg class="sidebaricon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> <use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg> </svg>
<ng-container i18n>Add Rule</ng-container> <ng-container i18n>Add Rule</ng-container>
</button> </button>
</h4> </h4>
<ul class="list-group"> <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>
<div class="col" i18n>Account</div> <div class="col" i18n>Account</div>
<div class="col" i18n>Actions</div> <div class="col" i18n>Actions</div>
</div> </div>
</li> </li>
<li *ngFor="let rule of mailRules" class="list-group-item"> @for (rule of mailRules; track rule) {
<div class="row"> <li class="list-group-item">
<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="row">
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</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"> <div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
<div class="btn-group"> <div class="col">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" [disabled]="!userCanEdit(rule)" class="btn btn-sm btn-outline-secondary" type="button" (click)="editMailRule(rule)"> <div class="btn-group">
<svg class="buttonicon-sm" fill="currentColor"> <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" [disabled]="!userCanEdit(rule)" class="btn btn-sm btn-outline-secondary" type="button" (click)="editMailRule(rule)">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#pencil" /> <use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container> </svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button> </button>
<button *pngxIfOwner="rule" class="btn btn-sm btn-outline-secondary" type="button" (click)="editPermissions(rule)"> <button *pngxIfOwner="rule" class="btn btn-sm btn-outline-secondary" type="button" (click)="editPermissions(rule)">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-lock" /> <use xlink:href="assets/bootstrap-icons.svg#person-lock" />
</svg>&nbsp;<ng-container i18n>Permissions</ng-container> </svg>&nbsp;<ng-container i18n>Permissions</ng-container>
</button> </button>
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailRule }" [disabled]="!userIsOwner(rule)" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)"> <button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailRule }" [disabled]="!userIsOwner(rule)" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)">
<svg class="buttonicon-sm" fill="currentColor"> <svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container> </svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</li> </li>
<li *ngIf="mailRules.length === 0" class="list-group-item" i18n>No mail rules defined.</li> }
</ul> @if (mailRules.length === 0) {
<li class="list-group-item" i18n>No mail rules defined.</li>
}
</ul>
</ng-container> </ng-container>
<div *ngIf="!mailAccounts || !mailRules"> @if (!mailAccounts || !mailRules) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> <div>
<div class="visually-hidden" i18n>Loading...</div> <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
</div> <div class="visually-hidden" i18n>Loading...</div>
</div>
}

View File

@ -2,112 +2,131 @@
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedObjects.size === 0"> <button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
<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>Clear selection</ng-container> </svg>&nbsp;<ng-container i18n>Clear selection</ng-container>
</button> </button>
<button type="button" class="btn btn-sm btn-outline-primary me-5" (click)="setPermissions()" [disabled]="!userOwnsAll || selectedObjects.size === 0"> <button type="button" class="btn btn-sm btn-outline-primary me-5" (click)="setPermissions()" [disabled]="!userOwnsAll || selectedObjects.size === 0">
<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#person-fill-lock" /> <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
</svg>&nbsp;<ng-container i18n>Permissions</ng-container> </svg>&nbsp;<ng-container i18n>Permissions</ng-container>
</button> </button>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }" i18n> <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }" i18n>
<svg class="sidebaricon me-1" fill="currentColor"> <svg class="sidebaricon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> <use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg> </svg>
Create Create
</button> </button>
</pngx-page-header> </pngx-page-header>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md mb-2 mb-xl-0"> <div class="col-md mb-2 mb-xl-0">
<div class="form-inline d-flex align-items-center"> <div class="form-inline d-flex align-items-center">
<label class="text-muted me-2 mb-0" i18n>Filter by:</label> <label class="text-muted me-2 mb-0" i18n>Filter by:</label>
<input class="form-control form-control-sm flex-fill w-auto" type="text" autofocus [(ngModel)]="nameFilter" (keyup)="onNameFilterKeyUp($event)" placeholder="Name" i18n-placeholder> <input class="form-control form-control-sm flex-fill w-auto" type="text" autofocus [(ngModel)]="nameFilter" (keyup)="onNameFilterKeyUp($event)" placeholder="Name" i18n-placeholder>
</div>
</div>
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
</div> </div>
</div>
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination> <div class="card border mb-3">
</div> <table class="table table-striped align-middle shadow-sm mb-0">
<thead>
<div class="card border mb-3"> <tr>
<table class="table table-striped align-middle shadow-sm mb-0"> <th scope="col">
<thead> <div class="form-check m-0 ms-2 me-n2">
<tr> <input type="checkbox" class="form-check-input" id="all-objects" [disabled]="data.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
<th scope="col"> <label class="form-check-label" for="all-objects"></label>
<div class="form-check m-0 ms-2 me-n2"> </div>
<input type="checkbox" class="form-check-input" id="all-objects" [disabled]="data.length === 0" (click)="toggleAll($event); $event.stopPropagation();"> </th>
<label class="form-check-label" for="all-objects"></label> <th scope="col" class="fw-normal" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
</div> <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> <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="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th> @for (column of extraColumns; track column) {
<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="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</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> <th scope="col" class="fw-normal" i18n>Actions</th>
<th scope="col" class="fw-normal" i18n>Actions</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> @if (isLoading) {
<tr *ngIf="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();"> }
<td> @for (object of data; track object) {
<div class="form-check m-0 ms-2 me-n2"> <tr (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();"> <td>
<label class="form-check-label" for="{{typeName}}{{object.id}}"></label> <div class="form-check m-0 ms-2 me-n2">
</div> <input type="checkbox" class="form-check-input" id="{{typeName}}{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();">
</td> <label class="form-check-label" for="{{typeName}}{{object.id}}"></label>
<td scope="row"><button class="btn btn-link ms-0 ps-0 text-start" (click)="openEditDialog(object)">{{ object.name }}</button> </td> </div>
<td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td> </td>
<td scope="row">{{ object.document_count }}</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" *ngFor="let column of extraColumns"> <td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
<div *ngIf="column.rendersHtml; else colValue" [innerHtml]="column.valueFn.call(null, object) | safeHtml"></div> <td scope="row">{{ object.document_count }}</td>
<ng-template #colValue>{{ column.valueFn.call(null, object) }}</ng-template> @for (column of extraColumns; track column) {
</td> <td scope="row">
<td scope="row"> @if (column.rendersHtml) {
<div class="btn-group d-block d-sm-none"> <div [innerHtml]="column.valueFn.call(null, object) | safeHtml"></div>
<div ngbDropdown class="d-inline-block"> } @else {
<button type="button" class="btn btn-link" id="actionsMenuMobile" ngbDropdownToggle> {{ column.valueFn.call(null, object) }}
<svg class="toolbaricon" fill="currentColor"> }
<use xlink:href="assets/bootstrap-icons.svg#three-dots-vertical" /> </td>
</svg> }
</button> <td scope="row">
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile"> <div class="btn-group d-block d-sm-none">
<button (click)="filterDocuments(object)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents</button> <div ngbDropdown class="d-inline-block">
<button (click)="openEditDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" ngbDropdownItem i18n>Edit</button> <button type="button" class="btn btn-link" id="actionsMenuMobile" ngbDropdownToggle>
<button class="text-danger" (click)="openDeleteDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" ngbDropdownItem i18n>Delete</button> <svg class="toolbaricon" fill="currentColor">
</div> <use xlink:href="assets/bootstrap-icons.svg#three-dots-vertical" />
</svg>
</button>
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
<button (click)="filterDocuments(object)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents</button>
<button (click)="openEditDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" ngbDropdownItem i18n>Edit</button>
<button class="text-danger" (click)="openDeleteDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" ngbDropdownItem i18n>Delete</button>
</div>
</div>
</div>
<div class="btn-group d-none d-sm-block">
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>&nbsp;<ng-container i18n>Documents</ng-container>
</button>
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div> </div>
</div>
<div class="btn-group d-none d-sm-block">
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>&nbsp;<ng-container i18n>Documents</ng-container>
</button>
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex mb-2" *ngIf="!isLoading"> @if (!isLoading) {
<div *ngIf="collectionSize > 0"> <div class="d-flex mb-2">
<ng-container i18n>{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</ng-container> @if (collectionSize > 0) {
<ng-container *ngIf="selectedObjects.size > 0">&nbsp;({{selectedObjects.size}} selected)</ng-container> <div>
</div> <ng-container i18n>{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</ng-container>
<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 (selectedObjects.size > 0) {
</div> &nbsp;({{selectedObjects.size}} selected)
}
</div>
}
@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>
}

View File

@ -38,6 +38,201 @@ export interface LanguageOption {
dateInputFormat?: string dateInputFormat?: string
} }
const LANGUAGE_OPTIONS = [
{
code: 'en-us',
name: $localize`English (US)`,
englishName: 'English (US)',
dateInputFormat: 'mm/dd/yyyy',
},
{
code: 'af-za',
name: $localize`Afrikaans`,
englishName: 'Afrikaans',
dateInputFormat: 'yyyy-mm-dd',
},
{
code: 'ar-ar',
name: $localize`Arabic`,
englishName: 'Arabic',
dateInputFormat: 'yyyy-mm-dd',
},
{
code: 'be-by',
name: $localize`Belarusian`,
englishName: 'Belarusian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'bg-bg',
name: $localize`Bulgarian`,
englishName: 'Bulgarian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'ca-es',
name: $localize`Catalan`,
englishName: 'Catalan',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'cs-cz',
name: $localize`Czech`,
englishName: 'Czech',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'da-dk',
name: $localize`Danish`,
englishName: 'Danish',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'de-de',
name: $localize`German`,
englishName: 'German',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'el-gr',
name: $localize`Greek`,
englishName: 'Greek',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'en-gb',
name: $localize`English (GB)`,
englishName: 'English (GB)',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'es-es',
name: $localize`Spanish`,
englishName: 'Spanish',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'fi-fi',
name: $localize`Finnish`,
englishName: 'Finnish',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'fr-fr',
name: $localize`French`,
englishName: 'French',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'hu-hu',
name: $localize`Hungarian`,
englishName: 'Hungarian',
dateInputFormat: 'yyyy.mm.dd',
},
{
code: 'it-it',
name: $localize`Italian`,
englishName: 'Italian',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'lb-lu',
name: $localize`Luxembourgish`,
englishName: 'Luxembourgish',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'nl-nl',
name: $localize`Dutch`,
englishName: 'Dutch',
dateInputFormat: 'dd-mm-yyyy',
},
{
code: 'no-no',
name: $localize`Norwegian`,
englishName: 'Norwegian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'pl-pl',
name: $localize`Polish`,
englishName: 'Polish',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'pt-br',
name: $localize`Portuguese (Brazil)`,
englishName: 'Portuguese (Brazil)',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'pt-pt',
name: $localize`Portuguese`,
englishName: 'Portuguese',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'ro-ro',
name: $localize`Romanian`,
englishName: 'Romanian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'ru-ru',
name: $localize`Russian`,
englishName: 'Russian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'sk-sk',
name: $localize`Slovak`,
englishName: 'Slovak',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'sl-si',
name: $localize`Slovenian`,
englishName: 'Slovenian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'sr-cs',
name: $localize`Serbian`,
englishName: 'Serbian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'sv-se',
name: $localize`Swedish`,
englishName: 'Swedish',
dateInputFormat: 'yyyy-mm-dd',
},
{
code: 'tr-tr',
name: $localize`Turkish`,
englishName: 'Turkish',
dateInputFormat: 'yyyy-mm-dd',
},
{
code: 'uk-ua',
name: $localize`Ukrainian`,
englishName: 'Ukrainian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'zh-cn',
name: $localize`Chinese Simplified`,
englishName: 'Chinese Simplified',
dateInputFormat: 'yyyy-mm-dd',
},
]
const ISO_LANGUAGE_OPTION: LanguageOption = {
code: 'iso-8601',
name: $localize`ISO 8601`,
dateInputFormat: 'yyyy-mm-dd',
}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@ -151,210 +346,14 @@ export class SettingsService {
} }
getLanguageOptions(): LanguageOption[] { getLanguageOptions(): LanguageOption[] {
const languages = [
{
code: 'en-us',
name: $localize`English (US)`,
englishName: 'English (US)',
dateInputFormat: 'mm/dd/yyyy',
},
{
code: 'af-za',
name: $localize`Afrikaans`,
englishName: 'Afrikaans',
dateInputFormat: 'yyyy-mm-dd',
},
{
code: 'ar-ar',
name: $localize`Arabic`,
englishName: 'Arabic',
dateInputFormat: 'yyyy-mm-dd',
},
{
code: 'be-by',
name: $localize`Belarusian`,
englishName: 'Belarusian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'bg-bg',
name: $localize`Bulgarian`,
englishName: 'Bulgarian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'ca-es',
name: $localize`Catalan`,
englishName: 'Catalan',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'cs-cz',
name: $localize`Czech`,
englishName: 'Czech',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'da-dk',
name: $localize`Danish`,
englishName: 'Danish',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'de-de',
name: $localize`German`,
englishName: 'German',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'el-gr',
name: $localize`Greek`,
englishName: 'Greek',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'en-gb',
name: $localize`English (GB)`,
englishName: 'English (GB)',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'es-es',
name: $localize`Spanish`,
englishName: 'Spanish',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'fi-fi',
name: $localize`Finnish`,
englishName: 'Finnish',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'fr-fr',
name: $localize`French`,
englishName: 'French',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'hu-hu',
name: $localize`Hungarian`,
englishName: 'Hungarian',
dateInputFormat: 'yyyy.mm.dd',
},
{
code: 'it-it',
name: $localize`Italian`,
englishName: 'Italian',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'lb-lu',
name: $localize`Luxembourgish`,
englishName: 'Luxembourgish',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'nl-nl',
name: $localize`Dutch`,
englishName: 'Dutch',
dateInputFormat: 'dd-mm-yyyy',
},
{
code: 'no-no',
name: $localize`Norwegian`,
englishName: 'Norwegian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'pl-pl',
name: $localize`Polish`,
englishName: 'Polish',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'pt-br',
name: $localize`Portuguese (Brazil)`,
englishName: 'Portuguese (Brazil)',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'pt-pt',
name: $localize`Portuguese`,
englishName: 'Portuguese',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'ro-ro',
name: $localize`Romanian`,
englishName: 'Romanian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'ru-ru',
name: $localize`Russian`,
englishName: 'Russian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'sk-sk',
name: $localize`Slovak`,
englishName: 'Slovak',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'sl-si',
name: $localize`Slovenian`,
englishName: 'Slovenian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'sr-cs',
name: $localize`Serbian`,
englishName: 'Serbian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'sv-se',
name: $localize`Swedish`,
englishName: 'Swedish',
dateInputFormat: 'yyyy-mm-dd',
},
{
code: 'tr-tr',
name: $localize`Turkish`,
englishName: 'Turkish',
dateInputFormat: 'yyyy-mm-dd',
},
{
code: 'uk-ua',
name: $localize`Ukrainian`,
englishName: 'Ukrainian',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'zh-cn',
name: $localize`Chinese Simplified`,
englishName: 'Chinese Simplified',
dateInputFormat: 'yyyy-mm-dd',
},
]
// Sort languages by localized name at runtime // Sort languages by localized name at runtime
languages.sort((a, b) => { return LANGUAGE_OPTIONS.sort((a, b) => {
return a.name < b.name ? -1 : 1 return a.name < b.name ? -1 : 1
}) })
return languages
} }
getDateLocaleOptions(): LanguageOption[] { getDateLocaleOptions(): LanguageOption[] {
let isoOption: LanguageOption = { return [ISO_LANGUAGE_OPTION].concat(this.getLanguageOptions())
code: 'iso-8601',
name: $localize`ISO 8601`,
dateInputFormat: 'yyyy-mm-dd',
}
return [isoOption].concat(this.getLanguageOptions())
} }
private getLanguageCookieName() { private getLanguageCookieName() {