mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Chore: Update Angular to v17 (#4980)
This commit is contained in:
		| @@ -1,32 +1,36 @@ | ||||
| <pngx-toasts></pngx-toasts> | ||||
|  | ||||
| <pngx-file-drop> | ||||
|     <ng-container content> | ||||
|         <router-outlet></router-outlet> | ||||
|     </ng-container> | ||||
|   <ng-container content> | ||||
|     <router-outlet></router-outlet> | ||||
|   </ng-container> | ||||
| </pngx-file-drop> | ||||
|  | ||||
| <tour-step-template> | ||||
|     <ng-template #tourStep let-step="step"> | ||||
|         <p class="tour-step-content" [innerHTML]="step?.content"></p> | ||||
|         <hr/> | ||||
|         <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> | ||||
|             <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"> | ||||
|                     <button class="btn btn-outline-danger" (click)="tourService.end()"> | ||||
|                         {{ step?.endBtnTitle }} | ||||
|                     </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> | ||||
|   <ng-template #tourStep let-step="step"> | ||||
|     <p class="tour-step-content" [innerHTML]="step?.content"></p> | ||||
|     <hr/> | ||||
|     <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> | ||||
|       <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"> | ||||
|           <button class="btn btn-outline-danger" (click)="tourService.end()"> | ||||
|             {{ step?.endBtnTitle }} | ||||
|           </button> | ||||
|         </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> | ||||
|   | ||||
| @@ -6,25 +6,35 @@ | ||||
| </pngx-page-header> | ||||
|  | ||||
| <ul ngbNav #nav="ngbNav" [(activeId)]="activeLog" (activeIdChange)="reloadLogs()" class="nav-tabs"> | ||||
|   <li *ngFor="let logFile of logFiles" [ngbNavItem]="logFile"> | ||||
|     <a ngbNavLink> | ||||
|       {{logFile}}.log | ||||
|     </a> | ||||
|   </li> | ||||
|   <div *ngIf="isLoading || !logFiles.length" class="ps-2 d-flex align-items-center"> | ||||
|     <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|     <ng-container *ngIf="!logFiles.length" i18n>Loading...</ng-container> | ||||
|   </div> | ||||
|   @for (logFile of logFiles; track logFile) { | ||||
|     <li [ngbNavItem]="logFile"> | ||||
|       <a ngbNavLink> | ||||
|         {{logFile}}.log | ||||
|       </a> | ||||
|     </li> | ||||
|   } | ||||
|   @if (isLoading || !logFiles.length) { | ||||
|     <div class="ps-2 d-flex align-items-center"> | ||||
|       <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|       @if (!logFiles.length) { | ||||
|         <ng-container i18n>Loading...</ng-container> | ||||
|       } | ||||
|     </div> | ||||
|   } | ||||
| </ul> | ||||
|  | ||||
| <div [ngbNavOutlet]="nav" class="mt-2"></div> | ||||
|  | ||||
| <div class="bg-dark p-3 text-light font-monospace log-container" #logContainer> | ||||
|   <div *ngIf="isLoading && logFiles.length"> | ||||
|     <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|     <ng-container i18n>Loading...</ng-container> | ||||
|   </div> | ||||
|   <p | ||||
|     class="m-0 p-0 log-entry-{{getLogLevel(log)}}" | ||||
|     *ngFor="let log of logs">{{log}}</p> | ||||
|   @if (isLoading && logFiles.length) { | ||||
|     <div> | ||||
|       <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|       <ng-container i18n>Loading...</ng-container> | ||||
|     </div> | ||||
|   } | ||||
|   @for (log of logs; track log) { | ||||
|     <p | ||||
|       class="m-0 p-0 log-entry-{{getLogLevel(log)}}" | ||||
|     >{{log}}</p> | ||||
|   } | ||||
| </div> | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| <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> | ||||
|   <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> | ||||
|       <svg class="sidebaricon ms-1" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/> | ||||
|       </svg> | ||||
|     <ng-container i18n>Open Django Admin</ng-container> | ||||
|     <svg class="sidebaricon ms-1" fill="currentColor"> | ||||
|       <use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/> | ||||
|     </svg> | ||||
|   </a> | ||||
| </pngx-page-header> | ||||
|  | ||||
| @@ -24,10 +24,16 @@ | ||||
|           <div class="col"> | ||||
|  | ||||
|             <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> | ||||
|  | ||||
|             <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> | ||||
| @@ -39,7 +45,11 @@ | ||||
|           <div class="col"> | ||||
|  | ||||
|             <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> | ||||
|  | ||||
|           </div> | ||||
| @@ -127,198 +137,202 @@ | ||||
|             <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"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|               </svg><ng-container i18n>Reset</ng-container> | ||||
|             </button> | ||||
|                 </svg><ng-container i18n>Reset</ng-container> | ||||
|               </button> | ||||
|             </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="offset-md-3 col"> | ||||
|             <p i18n> | ||||
|           <div class="row mb-3"> | ||||
|             <div class="offset-md-3 col"> | ||||
|               <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/> | ||||
|               Actual updating of the app must still be performed manually. | ||||
|             </p> | ||||
|             <p i18n> | ||||
|               <p i18n> | ||||
|               <em>No tracking data is collected by the app in any way.</em> | ||||
|             </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> | ||||
|  | ||||
|         <h4 class="mt-4" i18n>Bulk editing</h4> | ||||
|           <h4 class="mt-4" i18n>Bulk editing</h4> | ||||
|  | ||||
|         <div class="row mb-3"> | ||||
|           <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="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check> | ||||
|           <div class="row mb-3"> | ||||
|             <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="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <h4 class="mt-4" i18n>Notes</h4> | ||||
|           <h4 class="mt-4" i18n>Notes</h4> | ||||
|  | ||||
|         <div class="row mb-3"> | ||||
|           <div class="offset-md-3 col"> | ||||
|             <pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check> | ||||
|           <div class="row mb-3"> | ||||
|             <div class="offset-md-3 col"> | ||||
|               <pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|       </ng-template> | ||||
|     </li> | ||||
|         </ng-template> | ||||
|       </li> | ||||
|  | ||||
|     <li [ngbNavItem]="SettingsNavIDs.Permissions"> | ||||
|       <a ngbNavLink i18n>Permissions</a> | ||||
|       <ng-template ngbNavContent> | ||||
|       <li [ngbNavItem]="SettingsNavIDs.Permissions"> | ||||
|         <a ngbNavLink i18n>Permissions</a> | ||||
|         <ng-template ngbNavContent> | ||||
|  | ||||
|         <h4 i18n>Default Permissions</h4> | ||||
|           <h4 i18n>Default Permissions</h4> | ||||
|  | ||||
|         <div class="row mb-3"> | ||||
|           <div class="offset-md-3 col"> | ||||
|             <p i18n> | ||||
|           <div class="row mb-3"> | ||||
|             <div class="offset-md-3 col"> | ||||
|               <p i18n> | ||||
|               Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI | ||||
|             </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 class="row mb-3"> | ||||
|           <div class="col-md-3 col-form-label pt-0"> | ||||
|             <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 class="row mb-3"> | ||||
|             <div class="col-md-3 col-form-label pt-0"> | ||||
|               <span i18n>Default Owner</span> | ||||
|             </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 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> | ||||
|       </ng-template> | ||||
|     </li> | ||||
|  | ||||
|     <li [ngbNavItem]="SettingsNavIDs.Notifications"> | ||||
|       <a ngbNavLink i18n>Notifications</a> | ||||
|       <ng-template ngbNavContent> | ||||
|  | ||||
|         <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> <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 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="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 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="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 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 *ngIf="savedViews && savedViews.length === 0" i18n>No saved views defined.</div> | ||||
|  | ||||
|             <div *ngIf="!savedViews"> | ||||
|               <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 class="row mb-3"> | ||||
|             <div class="col-md-3 col-form-label pt-0"> | ||||
|               <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 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> | ||||
|     </li> | ||||
|   </ul> | ||||
|           <h4 i18n>Document processing</h4> | ||||
|  | ||||
|   <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> | ||||
| </form> | ||||
|         </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"> | ||||
|  | ||||
|             @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> <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> | ||||
|   | ||||
| @@ -48,6 +48,12 @@ enum SettingsNavIDs { | ||||
|   SavedViews = 4, | ||||
| } | ||||
|  | ||||
| const systemLanguage = { code: '', name: $localize`Use system language` } | ||||
| const systemDateFormat = { | ||||
|   code: '', | ||||
|   name: $localize`Use date format of display language`, | ||||
| } | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'pngx-settings', | ||||
|   templateUrl: './settings.component.html', | ||||
| @@ -512,15 +518,11 @@ export class SettingsComponent | ||||
|   } | ||||
|  | ||||
|   get displayLanguageOptions(): LanguageOption[] { | ||||
|     return [{ code: '', name: $localize`Use system language` }].concat( | ||||
|       this.settings.getLanguageOptions() | ||||
|     ) | ||||
|     return [systemLanguage].concat(this.settings.getLanguageOptions()) | ||||
|   } | ||||
|  | ||||
|   get dateLocaleOptions(): LanguageOption[] { | ||||
|     return [ | ||||
|       { code: '', name: $localize`Use date format of display language` }, | ||||
|     ].concat(this.settings.getDateLocaleOptions()) | ||||
|     return [systemDateFormat].concat(this.settings.getDateLocaleOptions()) | ||||
|   } | ||||
|  | ||||
|   get today() { | ||||
|   | ||||
| @@ -3,127 +3,153 @@ | ||||
|     <button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0"> | ||||
|       <svg class="sidebaricon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|       </svg> <ng-container i18n>Clear selection</ng-container> | ||||
|     </button> | ||||
|     <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"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#check2-all"/> | ||||
|       </svg> <ng-container i18n>{{dismissButtonText}}</ng-container> | ||||
|     </button> | ||||
|     <div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()"> | ||||
|       <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> | ||||
|     </div> | ||||
|   </div> | ||||
| </pngx-page-header> | ||||
|         </svg> <ng-container i18n>Clear selection</ng-container> | ||||
|       </button> | ||||
|       <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"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#check2-all"/> | ||||
|           </svg> <ng-container i18n>{{dismissButtonText}}</ng-container> | ||||
|         </button> | ||||
|         <div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()"> | ||||
|           <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> | ||||
|         </div> | ||||
|       </div> | ||||
|     </pngx-page-header> | ||||
|  | ||||
| <ng-container *ngIf="!tasksService.completedFileTasks && tasksService.loading"> | ||||
|   <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||
|   <div class="visually-hidden" i18n>Loading...</div> | ||||
| </ng-container> | ||||
|     @if (!tasksService.completedFileTasks && tasksService.loading) { | ||||
|       <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||
|       <div class="visually-hidden" i18n>Loading...</div> | ||||
|     } | ||||
|  | ||||
| <ng-template let-tasks="tasks" #tasksTemplate> | ||||
|   <table class="table table-striped align-middle border shadow-sm"> | ||||
|     <thead> | ||||
|       <tr> | ||||
|         <th scope="col"> | ||||
|           <div class="form-check"> | ||||
|             <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> | ||||
|           </div> | ||||
|         </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" *ngIf="activeTab !== 'started' && activeTab !== 'queued'" i18n>Results</th> | ||||
|         <th scope="col" class="d-table-cell d-lg-none" i18n>Info</th> | ||||
|         <th scope="col" i18n>Actions</th> | ||||
|       </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|       <ng-container *ngFor="let task of tasks | slice: (page-1) * pageSize : page * pageSize"> | ||||
|       <tr (click)="toggleSelected(task, $event); $event.stopPropagation();"> | ||||
|         <td> | ||||
|           <div class="form-check"> | ||||
|             <input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();"> | ||||
|             <label class="form-check-label" for="task{{task.id}}"></label> | ||||
|           </div> | ||||
|         </td> | ||||
|         <td class="overflow-auto name-col">{{ task.task_file_name }}</td> | ||||
|         <td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td> | ||||
|         <td class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'"> | ||||
|           <div *ngIf="task.result?.length > 50" class="result" (click)="expandTask(task); $event.stopPropagation();" | ||||
|             [ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body"> | ||||
|             <span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}…</span> | ||||
|           </div> | ||||
|           <span *ngIf="task.result?.length <= 50" class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span> | ||||
|           <ng-template #resultPopover> | ||||
|             <pre class="small mb-0">{{ task.result | slice:0:300 }}<ng-container *ngIf="task.result.length > 300">…</ng-container></pre> | ||||
|             <ng-container *ngIf="task.result?.length > 300"><br/><em>(<ng-container i18n>click for full output</ng-container>)</em></ng-container> | ||||
|           </ng-template> | ||||
|         </td> | ||||
|         <td class="d-lg-none"> | ||||
|           <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"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#info-circle" /> | ||||
|             </svg> | ||||
|           </button> | ||||
|         </td> | ||||
|         <td scope="row"> | ||||
|           <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 }"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#check"/> | ||||
|               </svg> <ng-container i18n>Dismiss</ng-container> | ||||
|             </button> | ||||
|             <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> | ||||
|               <button *ngIf="task.related_document" class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();"> | ||||
|                 <svg class="sidebaricon" fill="currentColor"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#file-text"/> | ||||
|                 </svg> <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> | ||||
|       </ng-container> | ||||
|     </tbody> | ||||
|   </table> | ||||
|     <ng-template let-tasks="tasks" #tasksTemplate> | ||||
|       <table class="table table-striped align-middle border shadow-sm"> | ||||
|         <thead> | ||||
|           <tr> | ||||
|             <th scope="col"> | ||||
|               <div class="form-check"> | ||||
|                 <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> | ||||
|               </div> | ||||
|             </th> | ||||
|             <th scope="col" i18n>Name</th> | ||||
|             <th scope="col" class="d-none d-lg-table-cell" i18n>Created</th> | ||||
|             @if (activeTab !== 'started' && activeTab !== 'queued') { | ||||
|               <th scope="col" class="d-none d-lg-table-cell" i18n>Results</th> | ||||
|             } | ||||
|             <th scope="col" class="d-table-cell d-lg-none" i18n>Info</th> | ||||
|             <th scope="col" i18n>Actions</th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           @for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) { | ||||
|             <tr (click)="toggleSelected(task, $event); $event.stopPropagation();"> | ||||
|               <td> | ||||
|                 <div class="form-check"> | ||||
|                   <input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();"> | ||||
|                   <label class="form-check-label" for="task{{task.id}}"></label> | ||||
|                 </div> | ||||
|               </td> | ||||
|               <td class="overflow-auto name-col">{{ task.task_file_name }}</td> | ||||
|               <td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td> | ||||
|               @if (activeTab !== 'started' && activeTab !== 'queued') { | ||||
|                 <td class="d-none d-lg-table-cell"> | ||||
|                   @if (task.result?.length > 50) { | ||||
|                     <div class="result" (click)="expandTask(task); $event.stopPropagation();" | ||||
|                       [ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body"> | ||||
|                       <span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}…</span> | ||||
|                     </div> | ||||
|                   } | ||||
|                   @if (task.result?.length <= 50) { | ||||
|                     <span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span> | ||||
|                   } | ||||
|                   <ng-template #resultPopover> | ||||
|                     <pre class="small mb-0">{{ task.result | slice:0:300 }}@if (task.result.length > 300) { | ||||
|                       … | ||||
|                     }</pre> | ||||
|                     @if (task.result?.length > 300) { | ||||
|                       <br/><em>(<ng-container i18n>click for full output</ng-container>)</em> | ||||
|                     } | ||||
|                   </ng-template> | ||||
|                 </td> | ||||
|               } | ||||
|               <td class="d-lg-none"> | ||||
|                 <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"> | ||||
|                     <use xlink:href="assets/bootstrap-icons.svg#info-circle" /> | ||||
|                   </svg> | ||||
|                 </button> | ||||
|               </td> | ||||
|               <td scope="row"> | ||||
|                 <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 }"> | ||||
|                     <svg class="sidebaricon" fill="currentColor"> | ||||
|                       <use xlink:href="assets/bootstrap-icons.svg#check"/> | ||||
|                       </svg> <ng-container i18n>Dismiss</ng-container> | ||||
|                     </button> | ||||
|                     <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> | ||||
|                       @if (task.related_document) { | ||||
|                         <button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();"> | ||||
|                           <svg class="sidebaricon" fill="currentColor"> | ||||
|                             <use xlink:href="assets/bootstrap-icons.svg#file-text"/> | ||||
|                             </svg> <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-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> | ||||
|     <ngb-pagination *ngIf="tasks.length > pageSize" [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination> | ||||
|   </div> | ||||
| </ng-template> | ||||
|           <div class="pb-3 d-sm-flex justify-content-between align-items-center"> | ||||
|             @if (tasks.length > 0) { | ||||
|               <div class="pb-2 pb-sm-0" i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</div> | ||||
|             } | ||||
|             @if (tasks.length > pageSize) { | ||||
|               <ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination> | ||||
|             } | ||||
|           </div> | ||||
|         </ng-template> | ||||
|  | ||||
| <ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)"> | ||||
|   <li ngbNavItem="failed"> | ||||
|     <a ngbNavLink i18n>Failed<span *ngIf="tasksService.failedFileTasks.length > 0" class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></a> | ||||
|     <ng-template ngbNavContent> | ||||
|       <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container> | ||||
|     </ng-template> | ||||
|   </li> | ||||
|   <li ngbNavItem="completed"> | ||||
|     <a ngbNavLink i18n>Complete<span *ngIf="tasksService.completedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span></a> | ||||
|     <ng-template ngbNavContent> | ||||
|       <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container> | ||||
|     </ng-template> | ||||
|   </li> | ||||
|   <li ngbNavItem="started"> | ||||
|     <a ngbNavLink i18n>Started<span *ngIf="tasksService.startedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span></a> | ||||
|     <ng-template ngbNavContent> | ||||
|       <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container> | ||||
|     </ng-template> | ||||
|   </li> | ||||
|   <li ngbNavItem="queued"> | ||||
|     <a ngbNavLink i18n>Queued<span *ngIf="tasksService.queuedFileTasks.length > 0" 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> | ||||
|         <ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)"> | ||||
|           <li ngbNavItem="failed"> | ||||
|             <a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) { | ||||
| <span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span> | ||||
| }</a> | ||||
|             <ng-template ngbNavContent> | ||||
|               <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container> | ||||
|             </ng-template> | ||||
|           </li> | ||||
|           <li ngbNavItem="completed"> | ||||
|             <a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) { | ||||
| <span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span> | ||||
| }</a> | ||||
|             <ng-template ngbNavContent> | ||||
|               <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container> | ||||
|             </ng-template> | ||||
|           </li> | ||||
|           <li ngbNavItem="started"> | ||||
|             <a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) { | ||||
| <span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span> | ||||
| }</a> | ||||
|             <ng-template ngbNavContent> | ||||
|               <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container> | ||||
|             </ng-template> | ||||
|           </li> | ||||
|           <li ngbNavItem="queued"> | ||||
|             <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> | ||||
|   | ||||
| @@ -1,97 +1,104 @@ | ||||
| <pngx-page-header title="Users & Groups" i18n-title> | ||||
| </pngx-page-header> | ||||
|  | ||||
| <ng-container *ngIf="users"> | ||||
|     <h4 class="d-flex"> | ||||
|         <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 }"> | ||||
|         <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|         </svg> | ||||
|         <ng-container i18n>Add User</ng-container> | ||||
|         </button> | ||||
|     </h4> | ||||
|     <ul class="list-group"> | ||||
|         <li class="list-group-item"> | ||||
| @if (users) { | ||||
|   <h4 class="d-flex"> | ||||
|     <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 }"> | ||||
|       <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|       </svg> | ||||
|       <ng-container i18n>Add User</ng-container> | ||||
|     </button> | ||||
|   </h4> | ||||
|   <ul class="list-group"> | ||||
|     <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="col" i18n>Username</div> | ||||
|             <div class="col" i18n>Name</div> | ||||
|             <div class="col" i18n>Groups</div> | ||||
|             <div class="col" i18n>Actions</div> | ||||
|         </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="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"> | ||||
|                 <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"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#pencil" /> | ||||
|                     </svg> <ng-container i18n>Edit</ng-container> | ||||
|               <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"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#pencil" /> | ||||
|                   </svg> <ng-container i18n>Edit</ng-container> | ||||
|                 </button> | ||||
|                 <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"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                   <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                     <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                     </svg> <ng-container i18n>Delete</ng-container> | ||||
|                 </button> | ||||
|                   </button> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         </li> | ||||
|     </ul> | ||||
| </ng-container> | ||||
|           </li> | ||||
|         } | ||||
|       </ul> | ||||
|     } | ||||
|  | ||||
| <ng-container *ngIf="groups"> | ||||
|     <h4 class="mt-4 d-flex"> | ||||
|     @if (groups) { | ||||
|       <h4 class="mt-4 d-flex"> | ||||
|         <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 }"> | ||||
|         <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|           <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|         </svg> | ||||
|         <ng-container i18n>Add Group</ng-container> | ||||
|           </svg> | ||||
|           <ng-container i18n>Add Group</ng-container> | ||||
|         </button> | ||||
|     </h4> | ||||
|     <ul *ngIf="groups.length > 0" class="list-group"> | ||||
|         <li class="list-group-item"> | ||||
|         <div class="row"> | ||||
|             <div class="col" i18n>Name</div> | ||||
|             <div class="col"></div> | ||||
|             <div class="col"></div> | ||||
|             <div class="col" i18n>Actions</div> | ||||
|         </div> | ||||
|         </li> | ||||
|  | ||||
|         <li *ngFor="let group of groups" 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)="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="btn-group"> | ||||
|                 <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"> | ||||
|       </h4> | ||||
|       @if (groups.length > 0) { | ||||
|         <ul class="list-group"> | ||||
|           <li class="list-group-item"> | ||||
|             <div class="row"> | ||||
|               <div class="col" i18n>Name</div> | ||||
|               <div class="col"></div> | ||||
|               <div class="col"></div> | ||||
|               <div class="col" i18n>Actions</div> | ||||
|             </div> | ||||
|           </li> | ||||
|           @for (group of groups; track group) { | ||||
|             <li 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)="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="btn-group"> | ||||
|                     <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" /> | ||||
|                     </svg> <ng-container i18n>Edit</ng-container> | ||||
|                 </button> | ||||
|                 <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"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                     </svg> <ng-container i18n>Delete</ng-container> | ||||
|                 </button> | ||||
|             </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         </li> | ||||
|         <li *ngIf="groups.length === 0" class="list-group-item" i18n>No groups defined</li> | ||||
|     </ul> | ||||
|                         </svg> <ng-container i18n>Edit</ng-container> | ||||
|                       </button> | ||||
|                       <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"> | ||||
|                           <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                           </svg> <ng-container i18n>Delete</ng-container> | ||||
|                         </button> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 </li> | ||||
|               } | ||||
|               @if (groups.length === 0) { | ||||
|                 <li class="list-group-item" i18n>No groups defined</li> | ||||
|               } | ||||
|             </ul> | ||||
|           } | ||||
|         } | ||||
|  | ||||
| </ng-container> | ||||
|  | ||||
| <div *ngIf="!users || !groups"> | ||||
|     <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||
|     <div class="visually-hidden" i18n>Loading...</div> | ||||
| </div> | ||||
|         @if (!users || !groups) { | ||||
|           <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> | ||||
|         } | ||||
|   | ||||
| @@ -17,11 +17,13 @@ | ||||
|       </svg> | ||||
|       <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> | ||||
|       <button type="button" *ngIf="!searchFieldEmpty" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()"> | ||||
|         <svg fill="currentColor" class="buttonicon-sm me-1"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|         </svg> | ||||
|       </button> | ||||
|       @if (!searchFieldEmpty) { | ||||
|         <button type="button" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()"> | ||||
|           <svg fill="currentColor" class="buttonicon-sm me-1"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|           </svg> | ||||
|         </button> | ||||
|       } | ||||
|     </form> | ||||
|   </div> | ||||
|   <ul ngbNav class="order-sm-3"> | ||||
| @@ -42,255 +44,282 @@ | ||||
|         <button ngbDropdownItem class="nav-link" (click)="editProfile()"> | ||||
|           <svg class="sidebaricon me-2" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#person"/> | ||||
|           </svg><ng-container i18n>My Profile</ng-container> | ||||
|         </button> | ||||
|         <a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }"> | ||||
|           <svg class="sidebaricon me-2" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#gear"/> | ||||
|           </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> <ng-container i18n>Dashboard</ng-container></span> | ||||
|             </svg><ng-container i18n>My Profile</ng-container> | ||||
|           </button> | ||||
|           <a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }"> | ||||
|             <svg class="sidebaricon me-2" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#gear"/> | ||||
|               </svg><ng-container i18n>Settings</ng-container> | ||||
|             </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> <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> {{view.name}}</span> | ||||
|             <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 *ngIf="settingsService.organizingSidebarSavedViews" 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 }"> | ||||
|           <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> {{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> <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> <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> <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> <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> <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> <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> <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> <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> <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> <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> <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> <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"> <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 }} | ||||
|               <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> | ||||
|               <div *ngIf="!settingsService.updateCheckingIsSet || appRemoteVersion" 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> | ||||
|             </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"> | ||||
|                   @if (slimSidebarEnabled) { | ||||
|                     <use xlink:href="assets/bootstrap-icons.svg#chevron-double-right"/> | ||||
|                   } | ||||
|                   @if (!slimSidebarEnabled) { | ||||
|                     <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> <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> <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> {{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> {{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> <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> <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> <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> <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> <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> <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> <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> <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> <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> <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> <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> <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"> <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? | ||||
|                     </a> | ||||
|                   </p> | ||||
|                 </ng-template> | ||||
|                 <ng-container *ngIf="settingsService.updateCheckingIsSet; else updateCheckNotSet"> | ||||
|                   <a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases" | ||||
|                   [ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> | ||||
|                     <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> | ||||
|                       <use xlink:href="assets/bootstrap-icons.svg#info-circle" /> | ||||
|                     </svg> | ||||
|                     <ng-container *ngIf="appRemoteVersion?.update_available" i18n>Update available</ng-container> | ||||
|                   </a> | ||||
|                 </ng-container> | ||||
|                 <ng-template #updateCheckNotSet> | ||||
|                   <a class="small text-decoration-none" routerLink="/settings" fragment="update-checking" | ||||
|                   [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"> | ||||
|                       <use xlink:href="assets/bootstrap-icons.svg#info-circle" /> | ||||
|                     </svg> | ||||
|                   </a> | ||||
|                 </ng-template> | ||||
|               </div> | ||||
|             </div> | ||||
|           </li> | ||||
|         </ul> | ||||
|       </div> | ||||
|     </nav> | ||||
|                                                           </p> | ||||
|                                                         </ng-template> | ||||
|                                                         @if (settingsService.updateCheckingIsSet) { | ||||
|                                                           @if (appRemoteVersion.update_available) { | ||||
|                                                             <a class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases" | ||||
|                                                               [ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> | ||||
|                                                               <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> | ||||
|                                                                 <use xlink:href="assets/bootstrap-icons.svg#info-circle" /> | ||||
|                                                               </svg> | ||||
|                                                               @if (appRemoteVersion?.update_available) { | ||||
|                                                                 <ng-container i18n>Update available</ng-container> | ||||
|                                                               } | ||||
|                                                             </a> | ||||
|                                                           } | ||||
|                                                         } @else { | ||||
|                                                           <a class="small text-decoration-none" routerLink="/settings" fragment="update-checking" | ||||
|                                                             [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"> | ||||
|                                                               <use xlink:href="assets/bootstrap-icons.svg#info-circle" /> | ||||
|                                                             </svg> | ||||
|                                                           </a> | ||||
|                                                         } | ||||
|                                                       </div> | ||||
|                                                     } | ||||
|                                                   </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'"> | ||||
|       <router-outlet></router-outlet> | ||||
|     </main> | ||||
|   </div> | ||||
| </div> | ||||
|                                           <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> | ||||
|                                           </main> | ||||
|                                         </div> | ||||
|                                       </div> | ||||
|   | ||||
| @@ -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)"> | ||||
|     <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"> | ||||
| @if (active) { | ||||
|   <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"/> | ||||
|     </svg> | ||||
|     <div *ngIf="isNumbered" class="number">{{number}}<span class="visually-hidden">selected</span></div> | ||||
|       </svg> | ||||
|     } | ||||
|     @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"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#x-lg"/> | ||||
|       <use xlink:href="assets/bootstrap-icons.svg#x-lg"/> | ||||
|     </svg> | ||||
| </button> | ||||
|   </button> | ||||
| } | ||||
|   | ||||
| @@ -1,24 +1,32 @@ | ||||
|     <div class="modal-header"> | ||||
|       <h4 class="modal-title" id="modal-basic-title">{{title}}</h4> | ||||
|       <button type="button" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||
|       </button> | ||||
|     </div> | ||||
|     <div class="modal-body"> | ||||
|       <p *ngIf="messageBold"><b>{{messageBold}}</b></p> | ||||
|       <p class="mb-0" *ngIf="message" [innerHTML]="message | safeHtml"></p> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|       <button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n> | ||||
| <div class="modal-header"> | ||||
|   <h4 class="modal-title" id="modal-basic-title">{{title}}</h4> | ||||
|   <button type="button" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||
|   </button> | ||||
| </div> | ||||
| <div class="modal-body"> | ||||
|   @if (messageBold) { | ||||
|     <p><b>{{messageBold}}</b></p> | ||||
|   } | ||||
|   @if (message) { | ||||
|     <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> | ||||
|       </button> | ||||
|       <button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled"> | ||||
|         <span> | ||||
|           {{btnCaption}} | ||||
|           <span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span> | ||||
|         </span> | ||||
|         <ngb-progressbar *ngIf="!confirmButtonEnabled" style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar> | ||||
|       </button> | ||||
|       <button *ngIf="alternativeBtnCaption" type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled"> | ||||
|         {{alternativeBtnCaption}} | ||||
|       </button> | ||||
|     </div> | ||||
|   <button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled"> | ||||
|     <span> | ||||
|       {{btnCaption}} | ||||
|       <span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span> | ||||
|     </span> | ||||
|     @if (!confirmButtonEnabled) { | ||||
|       <ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar> | ||||
|     } | ||||
|   </button> | ||||
|   @if (alternativeBtnCaption) { | ||||
|     <button type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled"> | ||||
|       {{alternativeBtnCaption}} | ||||
|     </button> | ||||
|   } | ||||
| </div> | ||||
|   | ||||
| @@ -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"> | ||||
|     {{title}} | ||||
|     <pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span> | ||||
|   </button> | ||||
|   <div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> | ||||
|     <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"> | ||||
|             <svg *ngIf="relativeDate === rd.id" fill="currentColor" class="buttonicon-sm"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#check"/> | ||||
|           </svg> | ||||
|             @if (relativeDate === rd.id) { | ||||
|               <svg fill="currentColor" class="buttonicon-sm"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#check"/> | ||||
|               </svg> | ||||
|             } | ||||
|           </div> | ||||
|           <div class="d-flex justify-content-between w-100 align-items-center ps-2"> | ||||
|             <div class="pe-2 pe-lg-4"> | ||||
| @@ -22,52 +25,57 @@ | ||||
|             </div> | ||||
|           </div> | ||||
|         </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 i18n>After</div> | ||||
|             <a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()"> | ||||
|         <div class="mb-2 d-flex flex-row w-100 justify-content-between small"> | ||||
|           <div i18n>After</div> | ||||
|           @if (dateAfter) { | ||||
|             <a class="btn btn-link p-0 m-0" (click)="clearAfter()"> | ||||
|               <svg fill="currentColor" class="buttonicon-sm"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|               </svg> | ||||
|               <small i18n>Clear</small> | ||||
|             </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 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> | ||||
|             <a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()"> | ||||
|         <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 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"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|               </svg> | ||||
|               <small i18n>Clear</small> | ||||
|             </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 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> | ||||
|   | ||||
| @@ -1,93 +1,95 @@ | ||||
| <form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off"> | ||||
|     <div class="modal-header"> | ||||
|       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||
|       <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||
|       </button> | ||||
|     </div> | ||||
|     <div class="modal-body"> | ||||
|       <div class="row"> | ||||
|         <div class="col-md-8"> | ||||
|           <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 class="modal-header"> | ||||
|     <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||
|     <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||
|     </button> | ||||
|   </div> | ||||
|   <div class="modal-body"> | ||||
|     <div class="row"> | ||||
|       <div class="col-md-8"> | ||||
|         <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> | ||||
|       </div> | ||||
|       <div class="row"> | ||||
|         <div class="col-md-4"> | ||||
|           <h5 class="border-bottom pb-2" i18n>Filters</h5> | ||||
|           <p class="small" i18n>Process documents that match <em>all</em> filters specified below.</p> | ||||
|           <pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select> | ||||
|           <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> | ||||
|           <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> | ||||
|           <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> | ||||
|         </div> | ||||
|         <div class="col"> | ||||
|           <div class="row"> | ||||
|             <div class="col"> | ||||
|               <h5 class="border-bottom pb-2" i18n>Assignments</h5> | ||||
|             </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 class="row"> | ||||
|       <div class="col-md-4"> | ||||
|         <h5 class="border-bottom pb-2" i18n>Filters</h5> | ||||
|         <p class="small" i18n>Process documents that match <em>all</em> filters specified below.</p> | ||||
|         <pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select> | ||||
|         <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> | ||||
|         <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> | ||||
|         <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> | ||||
|       </div> | ||||
|       <div class="col"> | ||||
|         <div class="row"> | ||||
|           <div class="col"> | ||||
|             <h5 class="border-bottom pb-2" i18n>Assignments</h5> | ||||
|           </div> | ||||
|           <div class="row"> | ||||
|             <div class="col"> | ||||
|               <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-tags [allowCreate]="false" i18n-title title="Assign tags" 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 correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></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 class="row"> | ||||
|           <div class="col"> | ||||
|             <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-tags [allowCreate]="false" i18n-title title="Assign tags" 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 correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></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 class="col"> | ||||
|             <pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select> | ||||
|               <div> | ||||
|                 <label class="form-label" i18n>Assign view permissions</label> | ||||
|                 <div class="mb-2"> | ||||
|                   <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="view" formControlName="assign_view_users"></pngx-permissions-user> | ||||
|                     </div> | ||||
|             <div> | ||||
|               <label class="form-label" i18n>Assign view permissions</label> | ||||
|               <div class="mb-2"> | ||||
|                 <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="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="view" formControlName="assign_view_groups"></pngx-permissions-group> | ||||
|                     </div> | ||||
|                   <div class="col-lg-9"> | ||||
|                     <pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user> | ||||
|                   </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 class="row"> | ||||
|                   <div class="col-lg-3"> | ||||
|                     <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label> | ||||
|                   </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 class="col-lg-9"> | ||||
|                     <pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group> | ||||
|                   </div> | ||||
|                   <small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small> | ||||
|                 </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 class="modal-footer"> | ||||
|       <span class="text-danger" *ngIf="error?.non_field_errors"><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="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> | ||||
|     </div> | ||||
|   </form> | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     @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="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> | ||||
|   </div> | ||||
| </form> | ||||
|   | ||||
| @@ -8,8 +8,12 @@ | ||||
|  | ||||
|     <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" novalidate></pngx-input-check> | ||||
|     @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" novalidate></pngx-input-check> | ||||
|     } | ||||
|  | ||||
|     <div *pngxIfOwner="object"> | ||||
|       <pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form> | ||||
|   | ||||
| @@ -7,7 +7,9 @@ | ||||
|   <div class="modal-body"> | ||||
|     <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> | ||||
|     <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 class="modal-footer"> | ||||
|     <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||
|   | ||||
| @@ -1,25 +1,29 @@ | ||||
| <form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off"> | ||||
|     <div class="modal-header"> | ||||
|       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||
|       <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||
|       </button> | ||||
|     </div> | ||||
|     <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="modal-header"> | ||||
|     <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||
|     <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||
|     </button> | ||||
|   </div> | ||||
|   <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> | ||||
|       @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 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 *pngxIfOwner="object"> | ||||
|       <pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form> | ||||
|     </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> | ||||
|   | ||||
| @@ -22,13 +22,15 @@ | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     <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> | ||||
|     <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> | ||||
|         <span class="visually-hidden mr-1" i18n>Loading...</span> | ||||
|       </ng-container> | ||||
|       } | ||||
|       <ng-container i18n>Test</ng-container> | ||||
|     </button> | ||||
|     <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||
|   | ||||
| @@ -26,19 +26,25 @@ | ||||
|       </div> | ||||
|       <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-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> | ||||
|         <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-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 *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> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   <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="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> | ||||
|   </div> | ||||
|   | ||||
| @@ -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="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-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> | ||||
|     @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> | ||||
|   | ||||
| @@ -1,26 +1,30 @@ | ||||
|   <form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off"> | ||||
|     <div class="modal-header"> | ||||
|       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||
|       <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||
|       </button> | ||||
| <form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off"> | ||||
|   <div class="modal-header"> | ||||
|     <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||
|     <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||
|     </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 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> | ||||
|       <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 *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> | ||||
|   </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> | ||||
|   | ||||
| @@ -4,49 +4,61 @@ | ||||
|       <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" /> | ||||
|     </svg> | ||||
|     <div class="d-none d-sm-inline"> {{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> | ||||
|     </ng-container> | ||||
|     } | ||||
|   </button> | ||||
|   <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}"> | ||||
|     <div class="list-group list-group-flush"> | ||||
|       <div *ngIf="!editing && manyToOne" class="list-group-item d-flex"> | ||||
|         <div class="btn-group btn-group-xs flex-fill" role="group"> | ||||
|           <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd_{{name}}" name="logicalOperatorAnd_{{name}}" value="and"> | ||||
|           <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="logicalOperatorOr_{{name}}" name="logicalOperatorOr_{{name}}" value="or"> | ||||
|           <label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label> | ||||
|       @if (!editing && manyToOne) { | ||||
|         <div class="list-group-item d-flex"> | ||||
|           <div class="btn-group btn-group-xs flex-fill" role="group"> | ||||
|             <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd_{{name}}" name="logicalOperatorAnd_{{name}}" value="and"> | ||||
|             <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="logicalOperatorOr_{{name}}" name="logicalOperatorOr_{{name}}" value="or"> | ||||
|             <label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div *ngIf="!editing && !manyToOne" class="list-group-item d-flex"> | ||||
|         <div class="btn-group btn-group-xs flex-fill" role="group"> | ||||
|           <input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionInclude_{{name}}" name="intersectionInclude_{{name}}" value="include"> | ||||
|           <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="intersectionExclude_{{name}}" name="intersectionExclude_{{name}}" value="exclude"> | ||||
|           <label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label> | ||||
|       } | ||||
|       @if (!editing && !manyToOne) { | ||||
|         <div class="list-group-item d-flex"> | ||||
|           <div class="btn-group btn-group-xs flex-fill" role="group"> | ||||
|             <input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionInclude_{{name}}" name="intersectionInclude_{{name}}" value="include"> | ||||
|             <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="intersectionExclude_{{name}}" name="intersectionExclude_{{name}}" value="exclude"> | ||||
|             <label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       } | ||||
|       <div class="list-group-item"> | ||||
|         <div class="input-group input-group-sm"> | ||||
|           <input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div *ngIf="selectionModel.items" class="items" #buttonItems> | ||||
|         <ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText; let i = index"> | ||||
|           <pngx-toggleable-dropdown-button | ||||
|             *ngIf="allowSelectNone || item.id" [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i - 1)" [disabled]="disabled"> | ||||
|           </pngx-toggleable-dropdown-button> | ||||
|         </ng-container> | ||||
|       </div> | ||||
|       <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"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#arrow-right" /> | ||||
|         </svg> | ||||
|       </button> | ||||
|       <div *ngIf="!editing && manyToOne" class="list-group-item list-group-item-note pt-1 pb-2"> | ||||
|         <small i18n>Click again to exclude items.</small> | ||||
|       </div> | ||||
|       @if (selectionModel.items) { | ||||
|         <div class="items" #buttonItems> | ||||
|           @for (item of selectionModel.itemsSorted | filter: filterText; track item; let i = $index) { | ||||
|             @if (allowSelectNone || item.id) { | ||||
|               <pngx-toggleable-dropdown-button | ||||
|                 [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i - 1)" [disabled]="disabled"> | ||||
|               </pngx-toggleable-dropdown-button> | ||||
|             } | ||||
|           } | ||||
|         </div> | ||||
|       } | ||||
|       @if (editing) { | ||||
|         <button class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled"> | ||||
|           <small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small> | ||||
|           <svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|             <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> | ||||
|   | ||||
| @@ -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"> | ||||
|   <div class="selected-icon me-1"> | ||||
|     <ng-container *ngIf="isChecked()"> | ||||
|     @if (isChecked()) { | ||||
|       <svg fill="currentColor" class="buttonicon-sm bi-check"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#check"/> | ||||
|       </svg> | ||||
|     </ng-container> | ||||
|     <ng-container *ngIf="isPartiallyChecked()"> | ||||
|     } | ||||
|     @if (isPartiallyChecked()) { | ||||
|       <svg fill="currentColor" class="buttonicon-sm bi-dash"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#dash"/> | ||||
|       </svg> | ||||
|     </ng-container> | ||||
|     <ng-container *ngIf="isExcluded()"> | ||||
|     } | ||||
|     @if (isExcluded()) { | ||||
|       <svg fill="currentColor" class="buttonicon-sm bi-x"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|       </svg> | ||||
|     </ng-container> | ||||
|     } | ||||
|   </div> | ||||
|   <div class="me-1"> | ||||
|     <pngx-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="false"></pngx-tag> | ||||
|     <ng-template #displayName><small>{{item.name}}</small></ng-template> | ||||
|     @if (isTag) { | ||||
|       <pngx-tag [tag]="item" [clickable]="false"></pngx-tag> | ||||
|     } @else { | ||||
|       <small>{{item.name}}</small> | ||||
|     } | ||||
|   </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> | ||||
|   | ||||
| @@ -1,19 +1,27 @@ | ||||
| <div class="mb-3"> | ||||
|   <div class="row"> | ||||
|     <div *ngIf="horizontal" class="d-flex align-items-center position-relative hidden-button-container col-md-3"> | ||||
|       <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> | ||||
|       <button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> | ||||
|         <svg class="sidebaricon" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|         </svg> <ng-container i18n>Remove</ng-container> | ||||
|       </button> | ||||
|     </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"> | ||||
|         <label *ngIf="!horizontal" class="form-check-label" [for]="inputId">{{title}}</label> | ||||
|         <div *ngIf="hint" class="form-text text-muted">{{hint}}</div> | ||||
|     @if (horizontal) { | ||||
|       <div class="d-flex align-items-center position-relative hidden-button-container col-md-3"> | ||||
|         <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label> | ||||
|         @if (removable) { | ||||
|           <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> | ||||
|             <svg class="sidebaricon" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|               </svg> <ng-container i18n>Remove</ng-container> | ||||
|             </button> | ||||
|           } | ||||
|         </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> | ||||
|   | ||||
| @@ -1,28 +1,32 @@ | ||||
| <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"> | ||||
|     <span class="input-group-text" [style.background-color]="value">   </span> | ||||
|  | ||||
|       <ng-template #popContent> | ||||
|         <div style="min-width: 200px;" class="pb-3"> | ||||
|           <color-slider [color]="value" (onChangeComplete)="colorChanged($event.color.hex)"></color-slider> | ||||
|         </div> | ||||
|     <ng-template #popContent> | ||||
|       <div style="min-width: 200px;" class="pb-3"> | ||||
|         <color-slider [color]="value" (onChangeComplete)="colorChanged($event.color.hex)"></color-slider> | ||||
|       </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()"> | ||||
|         <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="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> | ||||
|       </button> | ||||
|     <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"> | ||||
|         <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"/> | ||||
|       </svg> | ||||
|     </button> | ||||
|  | ||||
|     </div> | ||||
|     <small *ngIf="hint" class="form-text text-muted">{{hint}}</small> | ||||
|     <div class="invalid-feedback"> | ||||
|       {{error}} | ||||
|     </div> | ||||
|   </div> | ||||
|   @if (hint) { | ||||
|     <small class="form-text text-muted">{{hint}}</small> | ||||
|   } | ||||
|   <div class="invalid-feedback"> | ||||
|     {{error}} | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -2,36 +2,44 @@ | ||||
|   <div class="row"> | ||||
|     <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> | ||||
|       <button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> | ||||
|         <svg class="sidebaricon" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|         </svg> <ng-container i18n>Remove</ng-container> | ||||
|       </button> | ||||
|     </div> | ||||
|     <div class="position-relative" [class.col-md-9]="horizontal"> | ||||
|       <div class="input-group" [class.is-invalid]="error"> | ||||
|         <input #inputField class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10" | ||||
|               (dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)" | ||||
|               name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled"> | ||||
|         <button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled"> | ||||
|           <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="buttonicon"> | ||||
|             <use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use> | ||||
|           </svg> | ||||
|         </button> | ||||
|         <button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ fitlerButtonTitle }}"> | ||||
|           <svg class="buttonicon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#filter" /> | ||||
|           </svg> | ||||
|         </button> | ||||
|       @if (removable) { | ||||
|         <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> | ||||
|           <svg class="sidebaricon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|             </svg> <ng-container i18n>Remove</ng-container> | ||||
|           </button> | ||||
|         } | ||||
|       </div> | ||||
|       <div class="position-relative" [class.col-md-9]="horizontal"> | ||||
|         <div class="input-group" [class.is-invalid]="error"> | ||||
|           <input #inputField class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10" | ||||
|             (dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)" | ||||
|             name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled"> | ||||
|           <button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled"> | ||||
|             <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="buttonicon"> | ||||
|               <use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use> | ||||
|             </svg> | ||||
|           </button> | ||||
|           @if (showFilter) { | ||||
|             <button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="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 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>  | ||||
|             @for (s of getSuggestions(); track s) { | ||||
|               <a (click)="onSuggestionClick(s)" [routerLink]="[]">{{s}}</a>  | ||||
|             } | ||||
|           </small> | ||||
|         } | ||||
|       </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>  | ||||
|         <ng-container *ngFor="let s of getSuggestions()"> | ||||
|           <a (click)="onSuggestionClick(s)" [routerLink]="[]">{{s}}</a>  | ||||
|         </ng-container> | ||||
|       </small> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -1,12 +1,16 @@ | ||||
| <div class="mb-3 paperless-input-select" [class.disabled]="disabled"> | ||||
|     <div class="row"> | ||||
|       <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> | ||||
|         <button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> | ||||
|   <div class="row"> | ||||
|     <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal"> | ||||
|       @if (title) { | ||||
|         <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"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|           </svg> <ng-container i18n>Remove</ng-container> | ||||
|         </button> | ||||
|             </svg> <ng-container i18n>Remove</ng-container> | ||||
|           </button> | ||||
|         } | ||||
|       </div> | ||||
|       <div [class.col-md-9]="horizontal"> | ||||
|         <div> | ||||
| @@ -23,28 +27,30 @@ | ||||
|             [loading]="loading" | ||||
|             [typeahead]="documentsInput$" | ||||
|             (change)="onChange(selectedDocuments)"> | ||||
|                 <ng-template ng-label-tmp let-document="item"> | ||||
|                   <div class="d-flex align-items-center"> | ||||
|                     <svg class="sidebaricon" fill="currentColor" xmlns="http://www.w3.org/2000/svg" (click)="unselect(document)"> | ||||
|                       <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|                     </svg> | ||||
|                     <a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();"> | ||||
|                       <svg class="sidebaricon-sm me-1" fill="currentColor"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#file-text"/> | ||||
|                       </svg><span>{{document.title}}</span> | ||||
|                     </a> | ||||
|                   </div> | ||||
|                 </ng-template> | ||||
|                 <ng-template ng-loadingspinner-tmp> | ||||
|                   <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||
|                   <div class="visually-hidden" i18n>Loading...</div> | ||||
|                 </ng-template> | ||||
|                 <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> | ||||
|                 </ng-template> | ||||
|           </ng-select> | ||||
|             <ng-template ng-label-tmp let-document="item"> | ||||
|               <div class="d-flex align-items-center"> | ||||
|                 <svg class="sidebaricon" fill="currentColor" xmlns="http://www.w3.org/2000/svg" (click)="unselect(document)"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|                 </svg> | ||||
|                 <a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();"> | ||||
|                   <svg class="sidebaricon-sm me-1" fill="currentColor"> | ||||
|                     <use xlink:href="assets/bootstrap-icons.svg#file-text"/> | ||||
|                     </svg><span>{{document.title}}</span> | ||||
|                   </a> | ||||
|                 </div> | ||||
|               </ng-template> | ||||
|               <ng-template ng-loadingspinner-tmp> | ||||
|                 <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||
|                 <div class="visually-hidden" i18n>Loading...</div> | ||||
|               </ng-template> | ||||
|               <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> | ||||
|               </ng-template> | ||||
|             </ng-select> | ||||
|           </div> | ||||
|           @if (hint) { | ||||
|             <small class="form-text text-muted">{{hint}}</small> | ||||
|           } | ||||
|         </div> | ||||
|         <small *ngIf="hint" class="form-text text-muted">{{hint}}</small> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   | ||||
| @@ -2,21 +2,27 @@ | ||||
|   <div class="row"> | ||||
|     <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> | ||||
|       <button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> | ||||
|         <svg class="sidebaricon" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|         </svg> <ng-container i18n>Remove</ng-container> | ||||
|       </button> | ||||
|     </div> | ||||
|     <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> | ||||
|       @if (removable) { | ||||
|         <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> | ||||
|           <svg class="sidebaricon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|             </svg> <ng-container i18n>Remove</ng-container> | ||||
|           </button> | ||||
|         } | ||||
|       </div> | ||||
|       <div class="invalid-feedback position-absolute top-100"> | ||||
|         {{error}} | ||||
|       <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"> | ||||
|           @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> | ||||
|       <small *ngIf="hint" class="form-text text-muted">{{hint}}</small> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -2,14 +2,18 @@ | ||||
|   <label class="form-label" [for]="inputId">{{title}}</label> | ||||
|   <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"> | ||||
|     <button *ngIf="showReveal" type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle"> | ||||
|       <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#eye" /> | ||||
|       </svg> | ||||
|     </button> | ||||
|     @if (showReveal) { | ||||
|       <button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle"> | ||||
|         <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#eye" /> | ||||
|         </svg> | ||||
|       </button> | ||||
|     } | ||||
|   </div> | ||||
|   <div class="invalid-feedback"> | ||||
|     {{error}} | ||||
|   </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> | ||||
|   | ||||
| @@ -1,68 +1,75 @@ | ||||
| <ng-container *ngIf="!accordion"> | ||||
| @if (!accordion) { | ||||
|   <h5 i18n>Permissions</h5> | ||||
|   <ng-container [ngTemplateOutlet]="permissionsForm"></ng-container> | ||||
| </ng-container> | ||||
| <ng-container *ngIf="accordion"> | ||||
|   <ngb-accordion #acc="ngbAccordion" activeIds=""> | ||||
|     <ngb-panel i18n-title title="Edit Permissions"> | ||||
|       <ng-template ngbPanelContent> | ||||
|         <ng-container [ngTemplateOutlet]="permissionsForm"></ng-container> | ||||
|       </ng-template> | ||||
|     </ngb-panel> | ||||
|   </ngb-accordion> | ||||
| </ng-container> | ||||
| } | ||||
| @if (accordion) { | ||||
|   <div ngbAccordion activeIds=""> | ||||
|     <div ngbAccordionItem> | ||||
|       <h2 ngbAccordionHeader> | ||||
|         <button ngbAccordionButton i18n>Edit Permissions</button> | ||||
|       </h2> | ||||
|       <div ngbAccordionCollapse> | ||||
|         <div ngbAccordionBody> | ||||
|           <ng-template> | ||||
|             <ng-container [ngTemplateOutlet]="permissionsForm"></ng-container> | ||||
|           </ng-template> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| } | ||||
|  | ||||
| <ng-template #permissionsForm> | ||||
| <div [formGroup]="form"> | ||||
|   <div class="row"> | ||||
|     <div class="col-lg-3"> | ||||
|       <label class="form-label d-block my-2" i18n>Owner:</label> | ||||
|   <div [formGroup]="form"> | ||||
|     <div class="row"> | ||||
|       <div class="col-lg-3"> | ||||
|         <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 class="col-lg-9"> | ||||
|       <pngx-input-select [items]="users" bindLabel="username" formControlName="owner" [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 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> | ||||
|   <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> | ||||
|   | ||||
| @@ -1,57 +1,69 @@ | ||||
| <div class="mb-3 paperless-input-select" [class.disabled]="disabled"> | ||||
|   <div class="row"> | ||||
|     <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> | ||||
|       <button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> | ||||
|         <svg class="sidebaricon" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|         </svg> <ng-container i18n>Remove</ng-container> | ||||
|       </button> | ||||
|     </div> | ||||
|     <div [class.col-md-9]="horizontal"> | ||||
|       <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> | ||||
|         <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> | ||||
|       @if (title) { | ||||
|         <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"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|             </svg> <ng-container i18n>Remove</ng-container> | ||||
|           </button> | ||||
|         } | ||||
|       </div> | ||||
|       <div class="invalid-feedback"> | ||||
|         {{error}} | ||||
|       <div [class.col-md-9]="horizontal"> | ||||
|         <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>  | ||||
|             @for (s of getSuggestions(); track s) { | ||||
|               <a (click)="value = s.id; onChange(value)" [routerLink]="[]">{{s.name}}</a>  | ||||
|             } | ||||
|           </small> | ||||
|         } | ||||
|       </div> | ||||
|       <small *ngIf="hint" class="form-text text-muted">{{hint}}</small> | ||||
|       <small *ngIf="getSuggestions().length > 0"> | ||||
|         <span i18n>Suggestions:</span>  | ||||
|         <ng-container *ngFor="let s of getSuggestions()"> | ||||
|           <a (click)="value = s.id; onChange(value)" [routerLink]="[]">{{s.name}}</a>  | ||||
|         </ng-container> | ||||
|       </small> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -122,7 +122,7 @@ export class SelectComponent extends AbstractInputComponent<number> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   addItem(name: string) { | ||||
|   addItem(name: string = null) { | ||||
|     if (name) this.createNew.next(name) | ||||
|     else this.createNew.next(this._lastSearchTerm) | ||||
|     this.clearLastSearchTerm() | ||||
|   | ||||
| @@ -21,33 +21,45 @@ | ||||
|               <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"/> | ||||
|               </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> | ||||
|           </ng-template> | ||||
|           <ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm"> | ||||
|             <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> | ||||
|           </ng-template> | ||||
|         </ng-select> | ||||
|         <button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()" [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]="hasPrivate || this.value === null" i18n-title title="Filter documents with these Tags"> | ||||
|           <svg class="buttonicon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#filter" /> | ||||
|           </svg> | ||||
|         </button> | ||||
|         @if (allowCreate) { | ||||
|           <button class="btn btn-outline-secondary" type="button" (click)="createTag()" [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]="hasPrivate || this.value === null" i18n-title title="Filter documents with these Tags"> | ||||
|             <svg class="buttonicon" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#filter" /> | ||||
|             </svg> | ||||
|           </button> | ||||
|         } | ||||
|       </div> | ||||
|       <small class="form-text text-muted" *ngIf="hint">{{hint}}</small> | ||||
|       <small *ngIf="getSuggestions().length > 0" class="position-absolute top-100"> | ||||
|         <span i18n>Suggestions:</span>  | ||||
|         <ng-container *ngFor="let tag of getSuggestions()"> | ||||
|           <a (click)="addTag(tag.id)" [routerLink]="[]">{{tag?.name}}</a>  | ||||
|         </ng-container> | ||||
|       </small> | ||||
|       @if (hint) { | ||||
|         <small class="form-text text-muted">{{hint}}</small> | ||||
|       } | ||||
|       @if (getSuggestions().length > 0) { | ||||
|         <small class="position-absolute top-100"> | ||||
|           <span i18n>Suggestions:</span>  | ||||
|           @for (tag of getSuggestions(); track tag) { | ||||
|             <a (click)="addTag(tag.id)" [routerLink]="[]">{{tag?.name}}</a>  | ||||
|           } | ||||
|         </small> | ||||
|       } | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -1,9 +1,4 @@ | ||||
| import { | ||||
|   ComponentFixture, | ||||
|   TestBed, | ||||
|   fakeAsync, | ||||
|   tick, | ||||
| } from '@angular/core/testing' | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing' | ||||
| import { | ||||
|   FormsModule, | ||||
|   ReactiveFormsModule, | ||||
| @@ -15,7 +10,7 @@ import { | ||||
|   DEFAULT_MATCHING_ALGORITHM, | ||||
|   MATCH_ALL, | ||||
| } 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 { HttpClientTestingModule } from '@angular/common/http/testing' | ||||
| import { of } from 'rxjs' | ||||
|   | ||||
| @@ -2,18 +2,22 @@ | ||||
|   <div class="row"> | ||||
|     <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> | ||||
|       <button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> | ||||
|         <svg class="sidebaricon" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|         </svg> <ng-container i18n>Remove</ng-container> | ||||
|       </button> | ||||
|     </div> | ||||
|     <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"> | ||||
|       <small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small> | ||||
|       <div class="invalid-feedback position-absolute top-100"> | ||||
|         {{error}} | ||||
|       @if (removable) { | ||||
|         <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> | ||||
|           <svg class="sidebaricon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|             </svg> <ng-container i18n>Remove</ng-container> | ||||
|           </button> | ||||
|         } | ||||
|       </div> | ||||
|       <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"> | ||||
|         @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> | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| <div class="mb-3" [class.pb-3]="error"> | ||||
|     <div class="row"> | ||||
|       <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> | ||||
|         <button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> | ||||
|   <div class="row"> | ||||
|     <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> | ||||
|       @if (removable) { | ||||
|         <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)"> | ||||
|           <svg class="sidebaricon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|           </svg> <ng-container i18n>Remove</ng-container> | ||||
|         </button> | ||||
|             </svg> <ng-container i18n>Remove</ng-container> | ||||
|           </button> | ||||
|         } | ||||
|       </div> | ||||
|       <div [class.col-md-9]="horizontal"> | ||||
|         <div class="input-group" [class.is-invalid]="error"> | ||||
| @@ -20,7 +22,9 @@ | ||||
|             {{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> | ||||
|   </div> | ||||
|   | ||||
| @@ -2,7 +2,9 @@ | ||||
|   <div class="col-md text-truncate"> | ||||
|     <h3 class="text-truncate" style="line-height: 1.4"> | ||||
|       {{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> | ||||
|   </div> | ||||
|   <div class="btn-toolbar col col-md-auto"> | ||||
|   | ||||
| @@ -36,15 +36,6 @@ import { PDFSinglePageViewer } from 'pdfjs-dist/web/pdf_viewer' | ||||
|  | ||||
| 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 { | ||||
|   DISABLED, | ||||
|   ENABLED, | ||||
|   | ||||
| @@ -1,22 +1,24 @@ | ||||
| <div class="modal-header"> | ||||
|     <h4 class="modal-title" id="modal-basic-title">{{title}}</h4> | ||||
|     <button type="button" class="btn-close" aria-label="Close" (click)="cancelClicked()"> | ||||
|     </button> | ||||
|   </div> | ||||
|   <div class="modal-body"> | ||||
|   <h4 class="modal-title" id="modal-basic-title">{{title}}</h4> | ||||
|   <button type="button" class="btn-close" aria-label="Close" (click)="cancelClicked()"> | ||||
|   </button> | ||||
| </div> | ||||
| <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"> | ||||
|       <pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form> | ||||
|     </form> | ||||
|   <form [formGroup]="form"> | ||||
|     <pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form> | ||||
|   </form> | ||||
|  | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     <ng-container *ngIf="!buttonsEnabled"> | ||||
|       <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|       <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-primary" (click)="confirmClicked.emit(permissions)" [disabled]="!buttonsEnabled" i18n>Confirm</button> | ||||
|   </div> | ||||
| </div> | ||||
| <div class="modal-footer"> | ||||
|   @if (!buttonsEnabled) { | ||||
|     <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|     <span class="visually-hidden" i18n>Loading...</span> | ||||
|   } | ||||
|   <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> | ||||
| </div> | ||||
|   | ||||
| @@ -1,92 +1,106 @@ | ||||
| <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"> | ||||
|         <svg class="toolbaricon" fill="currentColor"> | ||||
|            <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" /> | ||||
|         </svg> | ||||
|         <div class="d-none d-sm-inline"> {{title}}</div> | ||||
|       <pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span> | ||||
|     </button> | ||||
|     <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"> | ||||
|             <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"> | ||||
|                     <svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.NONE" fill="currentColor" class="buttonicon-sm"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#check"/> | ||||
|                     </svg> | ||||
|                 </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"> | ||||
|                     <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> | ||||
|   <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="isActive ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled"> | ||||
|     <svg class="toolbaricon" fill="currentColor"> | ||||
|       <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" /> | ||||
|     </svg> | ||||
|     <div class="d-none d-sm-inline"> {{title}}</div> | ||||
|     <pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span> | ||||
|   </button> | ||||
|   <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"> | ||||
|       <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"> | ||||
|           @if (selectionModel.ownerFilter === OwnerFilterType.NONE) { | ||||
|             <svg fill="currentColor" class="buttonicon-sm"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#check"/> | ||||
|             </svg> | ||||
|           } | ||||
|         </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> | ||||
|   | ||||
| @@ -9,19 +9,23 @@ | ||||
|       <div class="col" i18n>Delete</div> | ||||
|       <div class="col" i18n>View</div> | ||||
|     </li> | ||||
|     <li class="list-group-item d-flex" *ngFor="let type of PermissionType | keyvalue" [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"> | ||||
|         <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> | ||||
|       </div> | ||||
|  | ||||
|       <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"> | ||||
|         <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> | ||||
|       </div> | ||||
|     </li> | ||||
|     <div *ngIf="error" class="invalid-feedback d-block">{{error}}</div> | ||||
|     @for (type of PermissionType | keyvalue; track type) { | ||||
|       <li class="list-group-item d-flex" [formGroupName]="type.key"> | ||||
|         <div class="col-3">{{type.key}}:</div> | ||||
|         <div class="col 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"> | ||||
|           <label class="form-check-label visually-hidden" for="{{type.key}}_all" i18n>All</label> | ||||
|         </div> | ||||
|         @for (action of PermissionAction | keyvalue; track action) { | ||||
|           <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}}"> | ||||
|             <label class="form-check-label visually-hidden" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label> | ||||
|           </div> | ||||
|         } | ||||
|       </li> | ||||
|     } | ||||
|     @if (error) { | ||||
|       <div class="invalid-feedback d-block">{{error}}</div> | ||||
|     } | ||||
|   </ul> | ||||
| </form> | ||||
|   | ||||
| @@ -1,22 +1,28 @@ | ||||
| <div class="preview-popup-container"> | ||||
|     <div *ngIf="error; else noError" class="w-100 h-100 position-relative"> | ||||
|         <p class="fst-italic position-absolute top-50 start-50 translate-middle" i18n>Error loading preview</p> | ||||
|   @if (error) { | ||||
|     <div class="w-100 h-100 position-relative"> | ||||
|       <p class="fst-italic position-absolute top-50 start-50 translate-middle" i18n>Error loading preview</p> | ||||
|     </div> | ||||
|     <ng-template #noError> | ||||
|         <object *ngIf="renderAsObject; else pngxViewer" [data]="previewURL | safeUrl" width="100%" class="bg-light" [class.p-2]="!isPdf"></object> | ||||
|         <ng-template #pngxViewer> | ||||
|             <div *ngIf="requiresPassword" class="w-100 h-100 position-relative"> | ||||
|                 <svg width="2em" height="2em" fill="currentColor" class="position-absolute top-50 start-50 translate-middle"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#file-earmark-lock"/> | ||||
|                 </svg> | ||||
|             </div> | ||||
|             <pngx-pdf-viewer *ngIf="!requiresPassword" | ||||
|                 [src]="previewURL" | ||||
|                 [original-size]="false" | ||||
|                 [show-borders]="true" | ||||
|                 [show-all]="true" | ||||
|                 (error)="onError($event)"> | ||||
|             </pngx-pdf-viewer> | ||||
|         </ng-template> | ||||
|     </ng-template> | ||||
|   } @else { | ||||
|     @if (renderAsObject) { | ||||
|       <object [data]="previewURL | safeUrl" width="100%" class="bg-light" [class.p-2]="!isPdf"></object> | ||||
|     } @else { | ||||
|       @if (requiresPassword) { | ||||
|         <div class="w-100 h-100 position-relative"> | ||||
|           <svg width="2em" height="2em" fill="currentColor" class="position-absolute top-50 start-50 translate-middle"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#file-earmark-lock"/> | ||||
|           </svg> | ||||
|         </div> | ||||
|       } | ||||
|       @if (!requiresPassword) { | ||||
|         <pngx-pdf-viewer | ||||
|           [src]="previewURL" | ||||
|           [original-size]="false" | ||||
|           [show-borders]="true" | ||||
|           [show-all]="true" | ||||
|           (error)="onError($event)"> | ||||
|         </pngx-pdf-viewer> | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| </div> | ||||
|   | ||||
| @@ -1,53 +1,57 @@ | ||||
| <form [formGroup]="form" (ngSubmit)="save()" autocomplete="off"> | ||||
|     <div class="modal-header"> | ||||
|       <h4 class="modal-title" id="modal-basic-title" i18n>Edit Profile</h4> | ||||
|       <button type="button" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||
|       </button> | ||||
|   <div class="modal-header"> | ||||
|     <h4 class="modal-title" id="modal-basic-title" i18n>Edit Profile</h4> | ||||
|     <button type="button" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||
|     </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 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> | ||||
|     <pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [showReveal]="true" autocomplete="current-password" [error]="error?.password"></pngx-input-password> | ||||
|     <div ngbAccordion> | ||||
|       <div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm" class="border-0 bg-transparent"> | ||||
|         <div ngbAccordionCollapse> | ||||
|           <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> | ||||
|           </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 ngbAccordion> | ||||
|           <div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm" class="border-0 bg-transparent"> | ||||
|             <div ngbAccordionCollapse> | ||||
|               <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> | ||||
|               </div> | ||||
|             </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text> | ||||
|     <pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text> | ||||
|     <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"> | ||||
|               @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> | ||||
|           <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> | ||||
|         <pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text> | ||||
|         <pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text> | ||||
|         <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 class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|       <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||
|   | ||||
| @@ -1,61 +1,79 @@ | ||||
| <div ngbDropdown> | ||||
|     <button class="btn btn-sm btn-outline-primary" id="shareLinksDropdown" [disabled]="disabled" ngbDropdownToggle> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#link" /> | ||||
|       </svg> | ||||
|       <div class="d-none d-sm-inline"> <ng-container i18n>Share Links</ng-container></div> | ||||
|     </button> | ||||
|     <div ngbDropdownMenu aria-labelledby="shareLinksDropdown" class="shadow share-links-dropdown"> | ||||
|       <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> | ||||
|   <button class="btn btn-sm btn-outline-primary" id="shareLinksDropdown" [disabled]="disabled" ngbDropdownToggle> | ||||
|     <svg class="toolbaricon" fill="currentColor"> | ||||
|       <use xlink:href="assets/bootstrap-icons.svg#link" /> | ||||
|     </svg> | ||||
|     <div class="d-none d-sm-inline"> <ng-container i18n>Share Links</ng-container></div> | ||||
|   </button> | ||||
|   <div ngbDropdownMenu aria-labelledby="shareLinksDropdown" class="shadow share-links-dropdown"> | ||||
|     <ul class="list-group list-group-flush"> | ||||
|       @if (!shareLinks || shareLinks.length === 0) { | ||||
|         <li class="list-group-item fst-italic small text-center text-secondary" i18n> | ||||
|           No existing links | ||||
|         </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"> | ||||
|             <input type="text" class="form-control" aria-label="Share link" [value]="getShareUrl(link)" readonly> | ||||
|             <span *ngIf="link.expiration" class="input-group-text"> | ||||
|               {{ getDaysRemaining(link) }} | ||||
|             </span> | ||||
|             @if (link.expiration) { | ||||
|               <span class="input-group-text"> | ||||
|                 {{ getDaysRemaining(link) }} | ||||
|               </span> | ||||
|             } | ||||
|             <button type="button" class="btn btn-sm btn-outline-primary" (click)="copy(link)"> | ||||
|               <svg class="buttonicon" fill="currentColor"> | ||||
|                 <use *ngIf="copied !== link.id" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" /> | ||||
|                 <use *ngIf="copied === link.id" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" /> | ||||
|               </svg><span class="visually-hidden" i18n>Copy</span> | ||||
|             </button> | ||||
|             <button *ngIf="canShare(link)" type="button" class="btn btn-sm btn-outline-primary" (click)="share(link)"> | ||||
|               <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"> | ||||
|                 @if (copied !== link.id) { | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#clipboard-fill" /> | ||||
|                 } | ||||
|                 @if (copied === link.id) { | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" /> | ||||
|                 } | ||||
|                 </svg><span class="visually-hidden" i18n>Copy</span> | ||||
|               </button> | ||||
|               @if (canShare(link)) { | ||||
|                 <button type="button" class="btn btn-sm btn-outline-primary" (click)="share(link)"> | ||||
|                   <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" /> | ||||
|                 </svg><span class="visually-hidden" i18n>Delete</span> | ||||
|             </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 === link.id" i18n>Copied!</span> | ||||
|         </li> | ||||
|         <li class="list-group-item pt-3 pb-2"> | ||||
|           <div class="input-group input-group-sm w-100"> | ||||
|             <div class="form-check form-switch ms-auto small"> | ||||
|               <input class="form-check-input" type="checkbox" role="switch" id="versionSwitch" [disabled]="!hasArchiveVersion" [(ngModel)]="useArchiveVersion"> | ||||
|               <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"> | ||||
|             <label class="input-group-text" for="addLink"><ng-container i18n>Expires</ng-container>:</label> | ||||
|             <select class="form-select form-select-sm" [(ngModel)]="expirationDays"> | ||||
|               <option *ngFor="let option of EXPIRATION_OPTIONS" [ngValue]="option.value">{{ option.label }}</option> | ||||
|             </select> | ||||
|             <button class="btn btn-sm btn-outline-primary ms-auto" type="button" (click)="createLink()" [disabled]="loading"> | ||||
|               <div *ngIf="loading" class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|               <svg *ngIf="!loading" class="buttonicon me-1" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#plus" /> | ||||
|               </svg> | ||||
|               <ng-container i18n>Create</ng-container> | ||||
|             </button> | ||||
|           </div> | ||||
|         </li> | ||||
|       </ul> | ||||
|     </div> | ||||
| </div> | ||||
|                     </svg><span class="visually-hidden" i18n>Delete</span> | ||||
|                   </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 === link.id" i18n>Copied!</span> | ||||
|               </li> | ||||
|             } | ||||
|             <li class="list-group-item pt-3 pb-2"> | ||||
|               <div class="input-group input-group-sm w-100"> | ||||
|                 <div class="form-check form-switch ms-auto small"> | ||||
|                   <input class="form-check-input" type="checkbox" role="switch" id="versionSwitch" [disabled]="!hasArchiveVersion" [(ngModel)]="useArchiveVersion"> | ||||
|                   <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"> | ||||
|                 <label class="input-group-text" for="addLink"><ng-container i18n>Expires</ng-container>:</label> | ||||
|                 <select class="form-select form-select-sm" [(ngModel)]="expirationDays"> | ||||
|                   @for (option of EXPIRATION_OPTIONS; track option) { | ||||
|                     <option [ngValue]="option.value">{{ option.label }}</option> | ||||
|                   } | ||||
|                 </select> | ||||
|                 <button class="btn btn-sm btn-outline-primary ms-auto" type="button" (click)="createLink()" [disabled]="loading"> | ||||
|                   @if (loading) { | ||||
|                     <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|                   } | ||||
|                   @if (!loading) { | ||||
|                     <svg class="buttonicon me-1" fill="currentColor"> | ||||
|                       <use xlink:href="assets/bootstrap-icons.svg#plus" /> | ||||
|                     </svg> | ||||
|                   } | ||||
|                   <ng-container i18n>Create</ng-container> | ||||
|                 </button> | ||||
|               </div> | ||||
|             </li> | ||||
|           </ul> | ||||
|         </div> | ||||
|       </div> | ||||
|   | ||||
| @@ -1,9 +1,15 @@ | ||||
| <ng-container *ngIf="tag !== undefined; else privateTag" > | ||||
|     <span *ngIf="!clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span> | ||||
|     <a [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a> | ||||
| </ng-container> | ||||
|  | ||||
| <ng-template #privateTag> | ||||
|     <span *ngIf="!clickable" class="badge private" i18n>Private</span> | ||||
|     <a [title]="linkTitle" *ngIf="clickable" class="badge private" i18n>Private</a> | ||||
| </ng-template> | ||||
| @if (tag !== undefined) { | ||||
|   @if (!clickable) { | ||||
|     <span class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span> | ||||
|   } | ||||
|   @if (clickable) { | ||||
|     <a [title]="linkTitle" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a> | ||||
|   } | ||||
| } @else { | ||||
|   @if (!clickable) { | ||||
|     <span class="badge private" i18n>Private</span> | ||||
|   } | ||||
|   @if (clickable) { | ||||
|     <a [title]="linkTitle" class="badge private" i18n>Private</a> | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,43 +1,58 @@ | ||||
| <ngb-toast | ||||
|   *ngFor="let toast of toasts" | ||||
|   [autohide]="true" [delay]="toast.delay" | ||||
|   [class]="toast.classname" | ||||
|   [class.mb-2]="true" | ||||
|   (shown)="onShow(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> | ||||
|   <span class="visually-hidden">{{ toast.delayRemaining / 1000 | number: '1.0-0' }} seconds</span> | ||||
|   <div class="d-flex align-items-top"> | ||||
|     <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"/> | ||||
|       <use *ngIf="toast.error" xlink:href="assets/bootstrap-icons.svg#exclamation-triangle"/> | ||||
|     </svg> | ||||
|     <div> | ||||
|       <p class="mb-0">{{toast.content}}</p> | ||||
|       <details *ngIf="toast.error"> | ||||
|         <div class="mt-2"> | ||||
|           <dl class="row mb-0" *ngIf="isDetailedError(toast.error)"> | ||||
|             <dt class="col-sm-3 fw-normal text-end">URL</dt> | ||||
|             <dd class="col-sm-9">{{ toast.error.url }}</dd> | ||||
|             <dt class="col-sm-3 fw-normal text-end" i18n>Status</dt> | ||||
|             <dd class="col-sm-9">{{ toast.error.status }} <em>{{ toast.error.statusText }}</em></dd> | ||||
|             <dt class="col-sm-3 fw-normal text-end" i18n>Error</dt> | ||||
|             <dd class="col-sm-9">{{ getErrorText(toast.error) }}</dd> | ||||
|           </dl> | ||||
|           <div class="row"> | ||||
|             <div class="col offset-sm-3"> | ||||
|               <button class="btn btn-sm btn-outline-dark" (click)="copyError(toast.error)"> | ||||
|                 <svg class="sidebaricon" fill="currentColor"> | ||||
|                   <use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard" /> | ||||
|                   <use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check" /> | ||||
|                 </svg> <ng-container i18n>Copy Raw Error</ng-container> | ||||
|               </button> | ||||
|             </div> | ||||
|           </div> | ||||
| @for (toast of toasts; track toast) { | ||||
|   <ngb-toast | ||||
|     [autohide]="true" [delay]="toast.delay" | ||||
|     [class]="toast.classname" | ||||
|     [class.mb-2]="true" | ||||
|     (shown)="onShow(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> | ||||
|     <span class="visually-hidden">{{ toast.delayRemaining / 1000 | number: '1.0-0' }} seconds</span> | ||||
|     <div class="d-flex align-items-top"> | ||||
|       <svg class="sidebaricon-sm mt-1 me-2 flex-shrink-0" fill="currentColor"> | ||||
|         @if (!toast.error) { | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#info-circle"/> | ||||
|         } | ||||
|         @if (toast.error) { | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#exclamation-triangle"/> | ||||
|         } | ||||
|       </svg> | ||||
|       <div> | ||||
|         <p class="mb-0">{{toast.content}}</p> | ||||
|         @if (toast.error) { | ||||
|           <details> | ||||
|             <div class="mt-2"> | ||||
|               @if (isDetailedError(toast.error)) { | ||||
|                 <dl class="row mb-0"> | ||||
|                   <dt class="col-sm-3 fw-normal text-end">URL</dt> | ||||
|                   <dd class="col-sm-9">{{ toast.error.url }}</dd> | ||||
|                   <dt class="col-sm-3 fw-normal text-end" i18n>Status</dt> | ||||
|                   <dd class="col-sm-9">{{ toast.error.status }} <em>{{ toast.error.statusText }}</em></dd> | ||||
|                   <dt class="col-sm-3 fw-normal text-end" i18n>Error</dt> | ||||
|                   <dd class="col-sm-9">{{ getErrorText(toast.error) }}</dd> | ||||
|                 </dl> | ||||
|               } | ||||
|               <div class="row"> | ||||
|                 <div class="col offset-sm-3"> | ||||
|                   <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> <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> | ||||
|       </details> | ||||
|       <p class="mb-0 mt-2" *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p> | ||||
|     </div> | ||||
|     <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> | ||||
|         <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> | ||||
|   } | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import { Clipboard } from '@angular/cdk/clipboard' | ||||
| }) | ||||
| export class ToastsComponent implements OnInit, OnDestroy { | ||||
|   constructor( | ||||
|     private toastService: ToastService, | ||||
|     public toastService: ToastService, | ||||
|     private clipboard: Clipboard | ||||
|   ) {} | ||||
|  | ||||
|   | ||||
| @@ -8,25 +8,31 @@ | ||||
|       cdkDropList | ||||
|       [cdkDropListDisabled]="settingsService.globalDropzoneActive" | ||||
|       (cdkDropListDropped)="onDrop($event)" | ||||
|     > | ||||
|       <div *ngIf="savedViewService.loading" class="col"> | ||||
|         <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|         <ng-container i18n>Loading...</ng-container> | ||||
|       </div> | ||||
|       > | ||||
|       @if (savedViewService.loading) { | ||||
|         <div class="col"> | ||||
|           <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|           <ng-container i18n>Loading...</ng-container> | ||||
|         </div> | ||||
|       } | ||||
|  | ||||
|       <div *ngIf="settingsService.offerTour()" class="col"> | ||||
|         <pngx-welcome-widget (dismiss)="completeTour()"></pngx-welcome-widget> | ||||
|       </div> | ||||
|       @if (settingsService.offerTour()) { | ||||
|         <div class="col"> | ||||
|           <pngx-welcome-widget (dismiss)="completeTour()"></pngx-welcome-widget> | ||||
|         </div> | ||||
|       } | ||||
|  | ||||
|       <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }"> | ||||
|         <div *ngFor="let v of dashboardViews" class="col"> | ||||
|           <pngx-saved-view-widget | ||||
|             [savedView]="v" | ||||
|             (cdkDragStarted)="onDragStart($event)" | ||||
|             (cdkDragEnded)="onDragEnd($event)" | ||||
|           > | ||||
|           </pngx-saved-view-widget> | ||||
|         </div> | ||||
|         @for (v of dashboardViews; track v) { | ||||
|           <div class="col"> | ||||
|             <pngx-saved-view-widget | ||||
|               [savedView]="v" | ||||
|               (cdkDragStarted)="onDragStart($event)" | ||||
|               (cdkDragEnded)="onDragEnd($event)" | ||||
|               > | ||||
|             </pngx-saved-view-widget> | ||||
|           </div> | ||||
|         } | ||||
|       </ng-container> | ||||
|     </div> | ||||
|   </div> | ||||
|   | ||||
| @@ -3,54 +3,63 @@ | ||||
|   [title]="savedView.name" | ||||
|   [loading]="loading" | ||||
|   [draggable]="savedView" | ||||
| > | ||||
|   > | ||||
|  | ||||
|   <a *ngIf="documents.length" class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a> | ||||
|   @if (documents.length) { | ||||
|     <a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a> | ||||
|   } | ||||
|  | ||||
|   <table *ngIf="documents.length; else empty" content class="table table-hover mb-0 align-middle"> | ||||
|     <thead> | ||||
|       <tr> | ||||
|         <th scope="col" i18n>Created</th> | ||||
|         <th scope="col" i18n>Title</th> | ||||
|         <th scope="col" class="d-none d-md-table-cell" i18n>Tags</th> | ||||
|         <th scope="col" class="d-none d-md-table-cell" i18n>Correspondent</th> | ||||
|       </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|       <tr *ngFor="let doc of documents" (mouseleave)="maybeClosePopover()"> | ||||
|         <td class="py-2 py-md-3"><a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.created_date | customDate}}</a></td> | ||||
|         <td class="py-2 py-md-3"> | ||||
|           <a routerLink="/documents/{{doc.id}}" title="Edit" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a> | ||||
|         </td> | ||||
|         <td class="py-2 py-md-3 d-none d-md-table-cell"> | ||||
|           <pngx-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag> | ||||
|         </td> | ||||
|         <td class="position-relative py-2 py-md-3 d-none d-md-table-cell"> | ||||
|           <a *ngIf="doc.correspondent !== null" class="btn-link text-dark text-decoration-none py-2 py-md-3" routerLink="/documents" [queryParams]="getCorrespondentQueryParams(doc.correspondent)">{{(doc.correspondent$ | async)?.name}}</a> | ||||
|           <div class="btn-group position-absolute top-50 end-0 translate-middle-y"> | ||||
|             <a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle" | ||||
|             [ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle" | ||||
|             autoClose="true" popoverClass="shadow popover-preview" container="body" (mouseenter)="mouseEnterPreviewButton(doc)" (mouseleave)="mouseLeavePreviewButton()" #popover="ngbPopover"> | ||||
|               <svg class="buttonicon-xs" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#eye"/> | ||||
|               </svg> | ||||
|             </a> | ||||
|             <ng-template #previewContent> | ||||
|               <pngx-preview-popup [document]="doc" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()"></pngx-preview-popup> | ||||
|             </ng-template> | ||||
|             <a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()"> | ||||
|               <svg class="buttonicon-xs" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#download"/> | ||||
|               </svg> | ||||
|             </a> | ||||
|           </div> | ||||
|         </td> | ||||
|       </tr> | ||||
|     </tbody> | ||||
|   </table> | ||||
|  | ||||
|   <ng-template #empty> | ||||
|   @if (documents.length) { | ||||
|     <table content class="table table-hover mb-0 align-middle"> | ||||
|       <thead> | ||||
|         <tr> | ||||
|           <th scope="col" i18n>Created</th> | ||||
|           <th scope="col" i18n>Title</th> | ||||
|           <th scope="col" class="d-none d-md-table-cell" i18n>Tags</th> | ||||
|           <th scope="col" class="d-none d-md-table-cell" i18n>Correspondent</th> | ||||
|         </tr> | ||||
|       </thead> | ||||
|       <tbody> | ||||
|         @for (doc of documents; track doc) { | ||||
|           <tr (mouseleave)="maybeClosePopover()"> | ||||
|             <td class="py-2 py-md-3"><a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.created_date | customDate}}</a></td> | ||||
|             <td class="py-2 py-md-3"> | ||||
|               <a routerLink="/documents/{{doc.id}}" title="Edit" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a> | ||||
|             </td> | ||||
|             <td class="py-2 py-md-3 d-none d-md-table-cell"> | ||||
|               @for (t of doc.tags$ | async; track t) { | ||||
|                 <pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag> | ||||
|               } | ||||
|             </td> | ||||
|             <td class="position-relative py-2 py-md-3 d-none d-md-table-cell"> | ||||
|               @if (doc.correspondent !== null) { | ||||
|                 <a class="btn-link text-dark text-decoration-none py-2 py-md-3" routerLink="/documents" [queryParams]="getCorrespondentQueryParams(doc.correspondent)">{{(doc.correspondent$ | async)?.name}}</a> | ||||
|               } | ||||
|               <div class="btn-group position-absolute top-50 end-0 translate-middle-y"> | ||||
|                 <a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle" | ||||
|                   [ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle" | ||||
|                   autoClose="true" popoverClass="shadow popover-preview" container="body" (mouseenter)="mouseEnterPreviewButton(doc)" (mouseleave)="mouseLeavePreviewButton()" #popover="ngbPopover"> | ||||
|                   <svg class="buttonicon-xs" fill="currentColor"> | ||||
|                     <use xlink:href="assets/bootstrap-icons.svg#eye"/> | ||||
|                   </svg> | ||||
|                 </a> | ||||
|                 <ng-template #previewContent> | ||||
|                   <pngx-preview-popup [document]="doc" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()"></pngx-preview-popup> | ||||
|                 </ng-template> | ||||
|                 <a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()"> | ||||
|                   <svg class="buttonicon-xs" fill="currentColor"> | ||||
|                     <use xlink:href="assets/bootstrap-icons.svg#download"/> | ||||
|                   </svg> | ||||
|                 </a> | ||||
|               </div> | ||||
|             </td> | ||||
|           </tr> | ||||
|         } | ||||
|       </tbody> | ||||
|     </table> | ||||
|   } @else { | ||||
|     <p i18n class="text-center text-muted mb-0 fst-italic">No documents</p> | ||||
|   </ng-template> | ||||
|   } | ||||
|  | ||||
|  | ||||
| </pngx-widget-frame> | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| <pngx-widget-frame title="Statistics" [loading]="loading" i18n-title> | ||||
|   <ng-container content> | ||||
|     <div class="list-group border-light"> | ||||
|       <a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to inbox" i18n-title href="javascript:void(0)" *ngIf="statistics?.documents_inbox !== null" (click)="goToInbox()"> | ||||
|         <ng-container i18n>Documents in inbox</ng-container>: | ||||
|         <span class="badge rounded-pill" [class.bg-primary]="statistics?.documents_inbox > 0" [class.bg-muted]="statistics?.documents_inbox === 0">{{statistics?.documents_inbox}}</span> | ||||
|       </a> | ||||
|       @if (statistics?.documents_inbox !== null) { | ||||
|         <a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to inbox" i18n-title href="javascript:void(0)" (click)="goToInbox()"> | ||||
|           <ng-container i18n>Documents in inbox</ng-container>: | ||||
|           <span class="badge rounded-pill" [class.bg-primary]="statistics?.documents_inbox > 0" [class.bg-muted]="statistics?.documents_inbox === 0">{{statistics?.documents_inbox}}</span> | ||||
|         </a> | ||||
|       } | ||||
|       <a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to documents" i18n-title routerLink="/documents/"> | ||||
|         <ng-container i18n>Total documents</ng-container>: | ||||
|         <span class="badge bg-primary rounded-pill">{{statistics?.documents_total}}</span> | ||||
| @@ -13,60 +15,74 @@ | ||||
|         <ng-container i18n>Total characters</ng-container>: | ||||
|         <span class="badge bg-secondary text-light rounded-pill">{{statistics?.character_count | number}}</span> | ||||
|       </div> | ||||
|       <div *ngIf="statistics?.document_file_type_counts?.length > 1" class="list-group-item filetypes"> | ||||
|         <div class="d-flex justify-content-between align-items-center my-2"> | ||||
|           <div class="progress flex-grow-1"> | ||||
|             <div *ngFor="let filetype of statistics?.document_file_type_counts; let i = index; let last = last" | ||||
|               class="progress-bar bg-primary" | ||||
|               role="progressbar" | ||||
|               [ngbPopover]="getFileTypeName(filetype)" | ||||
|               i18n-ngbPopover | ||||
|               triggers="mouseenter:mouseleave" | ||||
|               [attr.aria-label]="getFileTypeName(filetype)" | ||||
|               [class.me-1px]="!last" | ||||
|               [style.width]="getFileTypePercent(filetype) + '%'" | ||||
|               [style.opacity]="getItemOpacity(i)" | ||||
|               [attr.aria-valuenow]="getFileTypePercent(filetype)" | ||||
|               aria-valuemin="0" | ||||
|               aria-valuemax="100"> | ||||
|       @if (statistics?.document_file_type_counts?.length > 1) { | ||||
|         <div class="list-group-item filetypes"> | ||||
|           <div class="d-flex justify-content-between align-items-center my-2"> | ||||
|             <div class="progress flex-grow-1"> | ||||
|               @for (filetype of statistics?.document_file_type_counts; track filetype; let i = $index; let last = $last) { | ||||
|                 <div | ||||
|                   class="progress-bar bg-primary" | ||||
|                   role="progressbar" | ||||
|                   [ngbPopover]="getFileTypeName(filetype)" | ||||
|                   i18n-ngbPopover | ||||
|                   triggers="mouseenter:mouseleave" | ||||
|                   [attr.aria-label]="getFileTypeName(filetype)" | ||||
|                   [class.me-1px]="!last" | ||||
|                   [style.width]="getFileTypePercent(filetype) + '%'" | ||||
|                   [style.opacity]="getItemOpacity(i)" | ||||
|                   [attr.aria-valuenow]="getFileTypePercent(filetype)" | ||||
|                   aria-valuemin="0" | ||||
|                   aria-valuemax="100"> | ||||
|                 </div> | ||||
|               } | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="d-flex flex-wrap align-items-start"> | ||||
|           <div class="d-flex" *ngFor="let filetype of statistics?.document_file_type_counts; let i = index"> | ||||
|             <div class="text-nowrap me-2"> | ||||
|               <span class="badge rounded-pill bg-primary d-inline-block p-0 me-1" [style.opacity]="getItemOpacity(i)"></span> | ||||
|               <small class="text-nowrap"><span class="fw-bold">{{ getFileTypeExtension(filetype) }}</span> <span class="text-muted">({{getFileTypePercent(filetype) | number: '1.0-1'}}%)</span></small> | ||||
|             </div> | ||||
|           <div class="d-flex flex-wrap align-items-start"> | ||||
|             @for (filetype of statistics?.document_file_type_counts; track filetype; let i = $index) { | ||||
|               <div class="d-flex"> | ||||
|                 <div class="text-nowrap me-2"> | ||||
|                   <span class="badge rounded-pill bg-primary d-inline-block p-0 me-1" [style.opacity]="getItemOpacity(i)"></span> | ||||
|                   <small class="text-nowrap"><span class="fw-bold">{{ getFileTypeExtension(filetype) }}</span> <span class="text-muted">({{getFileTypePercent(filetype) | number: '1.0-1'}}%)</span></small> | ||||
|                 </div> | ||||
|               </div> | ||||
|             } | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       } | ||||
|     </div> | ||||
|  | ||||
|     <div class="list-group border-light mt-3"> | ||||
|       <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"> | ||||
|         <a *ngIf="statistics?.tag_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/tags/"> | ||||
|           <ng-container i18n>Tags</ng-container>: | ||||
|           <span class="badge bg-secondary text-light rounded-pill">{{statistics?.tag_count | number}}</span> | ||||
|         </a> | ||||
|         @if (statistics?.tag_count > 0) { | ||||
|           <a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/tags/"> | ||||
|             <ng-container i18n>Tags</ng-container>: | ||||
|             <span class="badge bg-secondary text-light rounded-pill">{{statistics?.tag_count | number}}</span> | ||||
|           </a> | ||||
|         } | ||||
|       </ng-container> | ||||
|       <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"> | ||||
|         <a *ngIf="statistics?.correspondent_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/correspondents/"> | ||||
|           <ng-container i18n>Correspondents</ng-container>: | ||||
|           <span class="badge bg-secondary text-light rounded-pill">{{statistics?.correspondent_count | number}}</span> | ||||
|         </a> | ||||
|         @if (statistics?.correspondent_count > 0) { | ||||
|           <a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/correspondents/"> | ||||
|             <ng-container i18n>Correspondents</ng-container>: | ||||
|             <span class="badge bg-secondary text-light rounded-pill">{{statistics?.correspondent_count | number}}</span> | ||||
|           </a> | ||||
|         } | ||||
|       </ng-container> | ||||
|       <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"> | ||||
|         <a *ngIf="statistics?.document_type_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documenttypes/"> | ||||
|           <ng-container i18n>Document Types</ng-container>: | ||||
|           <span class="badge bg-secondary text-light rounded-pill">{{statistics?.document_type_count | number}}</span> | ||||
|         </a> | ||||
|         @if (statistics?.document_type_count > 0) { | ||||
|           <a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documenttypes/"> | ||||
|             <ng-container i18n>Document Types</ng-container>: | ||||
|             <span class="badge bg-secondary text-light rounded-pill">{{statistics?.document_type_count | number}}</span> | ||||
|           </a> | ||||
|         } | ||||
|       </ng-container> | ||||
|       <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"> | ||||
|         <a *ngIf="statistics?.storage_path_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/storagepaths/"> | ||||
|           <ng-container i18n>Storage Paths</ng-container>: | ||||
|           <span class="badge bg-secondary text-light rounded-pill">{{statistics?.storage_path_count | number}}</span> | ||||
|         </a> | ||||
|         @if (statistics?.storage_path_count > 0) { | ||||
|           <a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/storagepaths/"> | ||||
|             <ng-container i18n>Storage Paths</ng-container>: | ||||
|             <span class="badge bg-secondary text-light rounded-pill">{{statistics?.storage_path_count | number}}</span> | ||||
|           </a> | ||||
|         } | ||||
|       </ng-container> | ||||
|     </div> | ||||
|   </ng-container> | ||||
|   | ||||
| @@ -8,32 +8,44 @@ | ||||
|     <div class="fixed-bottom p-2 p-md-4" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'offset-md-3 offset-lg-2'"> | ||||
|       <div class="row d-flex justify-content-end"> | ||||
|         <div class="col col-lg-4 col-xl-3 d-flex px-4 justify-content-between align-items-center"> | ||||
|           <p class="m-0 small text-muted" *ngIf="getStatus().length > 0">{{getStatusSummary()}}</p> | ||||
|           <a *ngIf="getStatusCompleted().length > 0" class="btn-link" (click)="dismissCompleted()" [routerLink]="[]" > | ||||
|             <span class="me-1" i18n="This button dismisses all status messages about processed documents on the dashboard (failed and successful)">Dismiss completed</span> | ||||
|             <svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-check2-all" viewBox="0 0 16 16"> | ||||
|               <path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7zm-4.208 7l-.896-.897.707-.707.543.543 6.646-6.647a.5.5 0 0 1 .708.708l-7 7a.5.5 0 0 1-.708 0z"/> | ||||
|               <path d="M5.354 7.146l.896.897-.707.707-.897-.896a.5.5 0 1 1 .708-.708z"/> | ||||
|             </svg> | ||||
|           </a> | ||||
|           @if (getStatus().length > 0) { | ||||
|             <p class="m-0 small text-muted">{{getStatusSummary()}}</p> | ||||
|           } | ||||
|           @if (getStatusCompleted().length > 0) { | ||||
|             <a class="btn-link" (click)="dismissCompleted()" [routerLink]="[]" > | ||||
|               <span class="me-1" i18n="This button dismisses all status messages about processed documents on the dashboard (failed and successful)">Dismiss completed</span> | ||||
|               <svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-check2-all" viewBox="0 0 16 16"> | ||||
|                 <path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7zm-4.208 7l-.896-.897.707-.707.543.543 6.646-6.647a.5.5 0 0 1 .708.708l-7 7a.5.5 0 0 1-.708 0z"/> | ||||
|                 <path d="M5.354 7.146l.896.897-.707.707-.897-.896a.5.5 0 1 1 .708-.708z"/> | ||||
|               </svg> | ||||
|             </a> | ||||
|           } | ||||
|         </div> | ||||
|       </div> | ||||
|       <div *ngFor="let status of getStatus()"> | ||||
|         <ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div *ngIf="getStatusHidden().length" class="alerts-hidden"> | ||||
|       <p *ngIf="!alertsExpanded" class="mt-3 mb-0 text-center"> | ||||
|         <span i18n="This is shown as a summary line when there are more than 5 document in the processing pipeline.">{getStatusHidden().length, plural, =1 {One more document} other {{{getStatusHidden().length}} more documents}}</span> | ||||
|          •  | ||||
|         <a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a> | ||||
|       </p> | ||||
|       <div #hiddenAlerts="ngbCollapse" [(ngbCollapse)]="!alertsExpanded"> | ||||
|         <div *ngFor="let status of getStatusHidden()"> | ||||
|       @for (status of getStatus(); track status) { | ||||
|         <div> | ||||
|           <ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container> | ||||
|         </div> | ||||
|       </div> | ||||
|       } | ||||
|     </div> | ||||
|     @if (getStatusHidden().length) { | ||||
|       <div class="alerts-hidden"> | ||||
|         @if (!alertsExpanded) { | ||||
|           <p class="mt-3 mb-0 text-center"> | ||||
|             <span i18n="This is shown as a summary line when there are more than 5 document in the processing pipeline.">{getStatusHidden().length, plural, =1 {One more document} other {{{getStatusHidden().length}} more documents}}</span> | ||||
|              •  | ||||
|             <a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a> | ||||
|           </p> | ||||
|         } | ||||
|         <div #hiddenAlerts="ngbCollapse" [(ngbCollapse)]="!alertsExpanded"> | ||||
|           @for (status of getStatusHidden(); track status) { | ||||
|             <div> | ||||
|               <ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container> | ||||
|             </div> | ||||
|           } | ||||
|         </div> | ||||
|       </div> | ||||
|     } | ||||
|   </div> | ||||
| </pngx-widget-frame> | ||||
|  | ||||
| @@ -42,17 +54,23 @@ | ||||
|     <div class="col col-lg-4 col-xl-3"> | ||||
|       <ngb-alert type="secondary" class="mt-2 mb-0" [dismissible]="isFinished(status)" (closed)="dismiss(status)"> | ||||
|         <h6 class="alert-heading">{{status.filename}}</h6> | ||||
|         <p class="mb-0 pb-1" *ngIf="!isFinished(status) || (isFinished(status) && !status.documentId)">{{status.message}}</p> | ||||
|         @if (!isFinished(status) || (isFinished(status) && !status.documentId)) { | ||||
|           <p class="mb-0 pb-1">{{status.message}}</p> | ||||
|         } | ||||
|         <ngb-progressbar [value]="status.getProgress()" [max]="1" [type]="getStatusColor(status)"></ngb-progressbar> | ||||
|         <div *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> | ||||
|           <div *ngIf="isFinished(status)"> | ||||
|             <button *ngIf="status.documentId" class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)"> | ||||
|               <small i18n>Open document</small> | ||||
|               <svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16"> | ||||
|                 <path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/> | ||||
|               </svg> | ||||
|             </button> | ||||
|           </div> | ||||
|           @if (isFinished(status)) { | ||||
|             <div> | ||||
|               @if (status.documentId) { | ||||
|                 <button class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)"> | ||||
|                   <small i18n>Open document</small> | ||||
|                   <svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16"> | ||||
|                     <path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/> | ||||
|                   </svg> | ||||
|                 </button> | ||||
|               } | ||||
|             </div> | ||||
|           } | ||||
|         </div> | ||||
|       </ngb-alert> | ||||
|     </div> | ||||
|   | ||||
| @@ -27,6 +27,24 @@ import { WidgetFrameComponent } from '../widget-frame/widget-frame.component' | ||||
| import { UploadFileWidgetComponent } from './upload-file-widget.component' | ||||
| import { DragDropModule } from '@angular/cdk/drag-drop' | ||||
|  | ||||
| const FAILED_STATUSES = [new FileStatus()] | ||||
| const WORKING_STATUSES = [new FileStatus(), new FileStatus()] | ||||
| const STARTED_STATUSES = [new FileStatus(), new FileStatus(), new FileStatus()] | ||||
| const SUCCESS_STATUSES = [ | ||||
|   new FileStatus(), | ||||
|   new FileStatus(), | ||||
|   new FileStatus(), | ||||
|   new FileStatus(), | ||||
| ] | ||||
| const DEFAULT_STATUSES = [ | ||||
|   new FileStatus(), | ||||
|   new FileStatus(), | ||||
|   new FileStatus(), | ||||
|   new FileStatus(), | ||||
|   new FileStatus(), | ||||
|   new FileStatus(), | ||||
| ] | ||||
|  | ||||
| describe('UploadFileWidgetComponent', () => { | ||||
|   let component: UploadFileWidgetComponent | ||||
|   let fixture: ComponentFixture<UploadFileWidgetComponent> | ||||
| @@ -150,41 +168,22 @@ function mockConsumerStatuses(consumerStatusService) { | ||||
|     .mockImplementation((phase) => { | ||||
|       switch (phase) { | ||||
|         case FileStatusPhase.FAILED: | ||||
|           return [new FileStatus()] | ||||
|           return FAILED_STATUSES | ||||
|         case FileStatusPhase.WORKING: | ||||
|           return [new FileStatus(), new FileStatus()] | ||||
|           return WORKING_STATUSES | ||||
|         case FileStatusPhase.STARTED: | ||||
|           return [new FileStatus(), new FileStatus(), new FileStatus()] | ||||
|           return STARTED_STATUSES | ||||
|         case FileStatusPhase.SUCCESS: | ||||
|           return [ | ||||
|             new FileStatus(), | ||||
|             new FileStatus(), | ||||
|             new FileStatus(), | ||||
|             new FileStatus(), | ||||
|           ] | ||||
|           return SUCCESS_STATUSES | ||||
|         case FileStatusPhase.UPLOADING: | ||||
|           return [partialUpload1, partialUpload2] | ||||
|         default: | ||||
|           return [ | ||||
|             new FileStatus(), | ||||
|             new FileStatus(), | ||||
|             new FileStatus(), | ||||
|             new FileStatus(), | ||||
|             new FileStatus(), | ||||
|             new FileStatus(), | ||||
|           ] | ||||
|           return DEFAULT_STATUSES | ||||
|       } | ||||
|     }) | ||||
|   jest | ||||
|     .spyOn(consumerStatusService, 'getConsumerStatusNotCompleted') | ||||
|     .mockImplementation(() => { | ||||
|       return [ | ||||
|         new FileStatus(), | ||||
|         new FileStatus(), | ||||
|         new FileStatus(), | ||||
|         new FileStatus(), | ||||
|         new FileStatus(), | ||||
|         new FileStatus(), | ||||
|       ] | ||||
|       return DEFAULT_STATUSES | ||||
|     }) | ||||
| } | ||||
|   | ||||
| @@ -2,17 +2,19 @@ | ||||
|   <div class="card-header"> | ||||
|     <div class="d-flex justify-content-between align-items-center"> | ||||
|       <div class="d-flex"> | ||||
|         <div *ngIf="draggable" class="ms-n2 me-1" cdkDragHandle> | ||||
|           <svg class="sidebaricon text-muted" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/> | ||||
|           </svg> | ||||
|         </div> | ||||
|         @if (draggable) { | ||||
|           <div class="ms-n2 me-1" cdkDragHandle> | ||||
|             <svg class="sidebaricon text-muted" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/> | ||||
|             </svg> | ||||
|           </div> | ||||
|         } | ||||
|         <h6 class="card-title mb-0">{{title}}</h6> | ||||
|       </div> | ||||
|       <ng-container *ngIf="loading"> | ||||
|       @if (loading) { | ||||
|         <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||
|         <div class="visually-hidden" i18n>Loading...</div> | ||||
|       </ng-container> | ||||
|       } | ||||
|       <ng-content select ="[header-buttons]"></ng-content> | ||||
|     </div> | ||||
|  | ||||
|   | ||||
| @@ -1,43 +1,47 @@ | ||||
| <pngx-page-header [(title)]="title"> | ||||
|     <ng-container *ngIf="getContentType() === 'application/pdf' && !useNativePdfViewer"> | ||||
|         <div class="input-group input-group-sm me-2 d-none d-md-flex"> | ||||
|             <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" /> | ||||
|             <div class="input-group-text" i18n>of {{previewNumPages}}</div> | ||||
|         </div> | ||||
|         <div class="input-group input-group-sm me-5 d-none d-md-flex"> | ||||
|             <button class="btn btn-outline-secondary" (click)="decreaseZoom()" i18n>-</button> | ||||
|             <select class="form-select" (change)="onZoomSelect($event)"> | ||||
|                 <option *ngFor="let setting of zoomSettings" [value]="setting" [selected]="previewZoomSetting === setting"> | ||||
|                     {{ getZoomSettingTitle(setting) }} | ||||
|                 </option> | ||||
|             </select> | ||||
|             <button class="btn btn-outline-secondary" (click)="increaseZoom()" i18n>+</button> | ||||
|         </div> | ||||
|     </ng-container> | ||||
|   @if (getContentType() === 'application/pdf' && !useNativePdfViewer) { | ||||
|     <div class="input-group input-group-sm me-2 d-none d-md-flex"> | ||||
|       <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" /> | ||||
|       <div class="input-group-text" i18n>of {{previewNumPages}}</div> | ||||
|     </div> | ||||
|     <div class="input-group input-group-sm me-5 d-none d-md-flex"> | ||||
|       <button class="btn btn-outline-secondary" (click)="decreaseZoom()" i18n>-</button> | ||||
|       <select class="form-select" (change)="onZoomSelect($event)"> | ||||
|         @for (setting of zoomSettings; track setting) { | ||||
|           <option [value]="setting" [selected]="previewZoomSetting === setting"> | ||||
|             {{ getZoomSettingTitle(setting) }} | ||||
|           </option> | ||||
|         } | ||||
|       </select> | ||||
|       <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 }"> | ||||
|         <svg class="buttonicon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|         </svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span> | ||||
|   <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"> | ||||
|       <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|       </svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span> | ||||
|     </button> | ||||
|  | ||||
|     <div class="btn-group me-2"> | ||||
|         <a [href]="downloadUrl" class="btn btn-sm btn-outline-primary"> | ||||
|             <svg class="buttonicon me-md-1" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#download" /> | ||||
|             </svg><span class="d-none d-lg-inline ps-1" i18n>Download</span> | ||||
|       <a [href]="downloadUrl" class="btn btn-sm btn-outline-primary"> | ||||
|         <svg class="buttonicon me-md-1" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#download" /> | ||||
|           </svg><span class="d-none d-lg-inline ps-1" i18n>Download</span> | ||||
|         </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> | ||||
|             <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 class="ms-auto" ngbDropdown> | ||||
|       <div class="ms-auto" ngbDropdown> | ||||
|         <button class="btn btn-sm btn-outline-primary me-2" id="actionsDropdown" ngbDropdownToggle> | ||||
|           <svg class="toolbaricon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#three-dots" /> | ||||
| @@ -46,240 +50,287 @@ | ||||
|         </button> | ||||
|         <div ngbDropdownMenu aria-labelledby="actionsDropdown" class="shadow"> | ||||
|           <button ngbDropdownItem (click)="redoOcr()" [disabled]="!userCanEdit"> | ||||
|               <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" /> | ||||
|             <svg class="buttonicon-sm" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" /> | ||||
|               </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"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#diagram-3" /> | ||||
|               </svg><span class="ps-1" i18n>More like this</span> | ||||
|           </button> | ||||
|         </div> | ||||
|     </div> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#diagram-3" /> | ||||
|                 </svg><span class="ps-1" i18n>More like this</span> | ||||
|               </button> | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|     <pngx-custom-fields-dropdown | ||||
|         *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }" | ||||
|         class="me-2" | ||||
|         [documentId]="documentId" | ||||
|         [disabled]="!userIsOwner" | ||||
|         [existingFields]="document?.custom_fields" | ||||
|         (created)="refreshCustomFields()" | ||||
|         (added)="addField($event)"> | ||||
|     </pngx-custom-fields-dropdown> | ||||
|           <pngx-custom-fields-dropdown | ||||
|             *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }" | ||||
|             class="me-2" | ||||
|             [documentId]="documentId" | ||||
|             [disabled]="!userIsOwner" | ||||
|             [existingFields]="document?.custom_fields" | ||||
|             (created)="refreshCustomFields()" | ||||
|             (added)="addField($event)"> | ||||
|           </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-page-header> | ||||
|           <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> | ||||
|  | ||||
| <div class="row"> | ||||
|     <div class="col-md-6 col-xl-4 mb-4"> | ||||
|         <div class="row"> | ||||
|           <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"> | ||||
|                     <button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Close" (click)="close()"> | ||||
|                         <svg class="buttonicon" fill="currentColor"> | ||||
|                             <use xlink:href="assets/bootstrap-icons.svg#x" /> | ||||
|                         </svg> | ||||
|                     </button> | ||||
|                     <button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Previous" (click)="previousDoc()" [disabled]="!hasPrevious()"> | ||||
|                         <svg class="buttonicon" fill="currentColor"> | ||||
|                             <use xlink:href="assets/bootstrap-icons.svg#arrow-left" /> | ||||
|                         </svg> | ||||
|                     </button> | ||||
|                     <button type="button" class="btn btn-sm btn-outline-secondary"  i18n-title title="Next" (click)="nextDoc()" [disabled]="!hasNext()"> | ||||
|                         <svg class="buttonicon" fill="currentColor"> | ||||
|                             <use xlink:href="assets/bootstrap-icons.svg#arrow-right" /> | ||||
|                         </svg> | ||||
|                     </button> | ||||
|                   <button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Close" (click)="close()"> | ||||
|                     <svg class="buttonicon" fill="currentColor"> | ||||
|                       <use xlink:href="assets/bootstrap-icons.svg#x" /> | ||||
|                     </svg> | ||||
|                   </button> | ||||
|                   <button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Previous" (click)="previousDoc()" [disabled]="!hasPrevious()"> | ||||
|                     <svg class="buttonicon" fill="currentColor"> | ||||
|                       <use xlink:href="assets/bootstrap-icons.svg#arrow-left" /> | ||||
|                     </svg> | ||||
|                   </button> | ||||
|                   <button type="button" class="btn btn-sm btn-outline-secondary"  i18n-title title="Next" (click)="nextDoc()" [disabled]="!hasNext()"> | ||||
|                     <svg class="buttonicon" fill="currentColor"> | ||||
|                       <use xlink:href="assets/bootstrap-icons.svg#arrow-right" /> | ||||
|                     </svg> | ||||
|                   </button> | ||||
|                 </div> | ||||
|  | ||||
|                 <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"> | ||||
|                     <a ngbNavLink i18n>Details</a> | ||||
|                     <ng-template ngbNavContent> | ||||
|                         <div> | ||||
|                             <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-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> | ||||
|                             <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> | ||||
|                             <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> | ||||
|                             <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> | ||||
|                             <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"> | ||||
|                                 <div [formGroup]="customFieldFormFields.controls[i]" [ngSwitch]="getCustomFieldFromInstance(fieldInstance)?.data_type"> | ||||
|                                     <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> | ||||
|                                     <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> | ||||
|                                     <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-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> | ||||
|                                     <pngx-input-check *ngSwitchCase="PaperlessCustomFieldDataType.Boolean" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true"></pngx-input-check> | ||||
|                                     <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> | ||||
|                                 </div> | ||||
|                             </ng-container> | ||||
|                   <a ngbNavLink i18n>Details</a> | ||||
|                   <ng-template ngbNavContent> | ||||
|                     <div> | ||||
|                       <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-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> | ||||
|                       <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> | ||||
|                       <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> | ||||
|                       <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> | ||||
|                       <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> | ||||
|                       @for (fieldInstance of document?.custom_fields; track fieldInstance; let i = $index) { | ||||
|                         <div [formGroup]="customFieldFormFields.controls[i]"> | ||||
|                           @switch (getCustomFieldFromInstance(fieldInstance)?.data_type) { | ||||
|                             @case (PaperlessCustomFieldDataType.String) { | ||||
|                               <pngx-input-text formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-text> | ||||
|                             } | ||||
|                             @case (PaperlessCustomFieldDataType.Date) { | ||||
|                               <pngx-input-date formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-date> | ||||
|                             } | ||||
|                             @case (PaperlessCustomFieldDataType.Integer) { | ||||
|                               <pngx-input-number formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [error]="getCustomFieldError(i)"></pngx-input-number> | ||||
|                             } | ||||
|                             @case (PaperlessCustomFieldDataType.Float) { | ||||
|                               <pngx-input-number formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [step]=".1" [error]="getCustomFieldError(i)"></pngx-input-number> | ||||
|                             } | ||||
|                             @case (PaperlessCustomFieldDataType.Monetary) { | ||||
|                               <pngx-input-number formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [step]=".01" [error]="getCustomFieldError(i)"></pngx-input-number> | ||||
|                             } | ||||
|                             @case (PaperlessCustomFieldDataType.Boolean) { | ||||
|                               <pngx-input-check formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true"></pngx-input-check> | ||||
|                             } | ||||
|                             @case (PaperlessCustomFieldDataType.Url) { | ||||
|                               <pngx-input-url formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-url> | ||||
|                             } | ||||
|                             @case (PaperlessCustomFieldDataType.DocumentLink) { | ||||
|                               <pngx-input-document-link formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [parentDocumentID]="documentId" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-document-link> | ||||
|                             } | ||||
|                           } | ||||
|                         </div> | ||||
|                       } | ||||
|                     </div> | ||||
|  | ||||
|                         <div class="d-flex border-top pt-3"> | ||||
|                             <ng-container *ngTemplateOutlet="saveButtons"></ng-container> | ||||
|                         </div> | ||||
|                     </ng-template> | ||||
|                     <div class="d-flex border-top pt-3"> | ||||
|                       <ng-container *ngTemplateOutlet="saveButtons"></ng-container> | ||||
|                     </div> | ||||
|                   </ng-template> | ||||
|                 </li> | ||||
|  | ||||
|                 <li [ngbNavItem]="DocumentDetailNavIDs.Content"> | ||||
|                     <a ngbNavLink i18n>Content</a> | ||||
|                     <ng-template ngbNavContent> | ||||
|                         <div> | ||||
|                             <textarea class="form-control" id="content" rows="20" formControlName='content' [class.rtl]="isRTL"></textarea> | ||||
|                         </div> | ||||
|                     </ng-template> | ||||
|                   <a ngbNavLink i18n>Content</a> | ||||
|                   <ng-template ngbNavContent> | ||||
|                     <div> | ||||
|                       <textarea class="form-control" id="content" rows="20" formControlName='content' [class.rtl]="isRTL"></textarea> | ||||
|                     </div> | ||||
|                   </ng-template> | ||||
|                 </li> | ||||
|  | ||||
|                 <li [ngbNavItem]="DocumentDetailNavIDs.Metadata"> | ||||
|                     <a ngbNavLink i18n>Metadata</a> | ||||
|                     <ng-template ngbNavContent> | ||||
|                   <a ngbNavLink i18n>Metadata</a> | ||||
|                   <ng-template ngbNavContent> | ||||
|  | ||||
|                         <table class="table table-borderless" *ngIf="document"> | ||||
|                             <tbody> | ||||
|                                 <tr> | ||||
|                                     <td i18n>Date modified</td> | ||||
|                                     <td>{{document.modified | customDate}}</td> | ||||
|                                 </tr> | ||||
|                                 <tr> | ||||
|                                     <td i18n>Date added</td> | ||||
|                                     <td>{{document.added | customDate}}</td> | ||||
|                                 </tr> | ||||
|                                 <tr> | ||||
|                                     <td i18n>Media filename</td> | ||||
|                                     <td>{{metadata?.media_filename}}</td> | ||||
|                                 </tr> | ||||
|                                 <tr> | ||||
|                                     <td i18n>Original filename</td> | ||||
|                                     <td>{{metadata?.original_filename}}</td> | ||||
|                                 </tr> | ||||
|                                 <tr> | ||||
|                                     <td i18n>Original MD5 checksum</td> | ||||
|                                     <td>{{metadata?.original_checksum}}</td> | ||||
|                                 </tr> | ||||
|                                 <tr> | ||||
|                                     <td i18n>Original file size</td> | ||||
|                                     <td>{{metadata?.original_size | fileSize}}</td> | ||||
|                                 </tr> | ||||
|                                 <tr> | ||||
|                                     <td i18n>Original mime type</td> | ||||
|                                     <td>{{metadata?.original_mime_type}}</td> | ||||
|                                 </tr> | ||||
|                                 <tr *ngIf="metadata?.has_archive_version"> | ||||
|                                     <td i18n>Archive MD5 checksum</td> | ||||
|                                     <td>{{metadata?.archive_checksum}}</td> | ||||
|                                 </tr> | ||||
|                                 <tr *ngIf="metadata?.has_archive_version"> | ||||
|                                     <td i18n>Archive file size</td> | ||||
|                                     <td>{{metadata?.archive_size | fileSize}}</td> | ||||
|                                 </tr> | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     @if (document) { | ||||
|                       <table class="table table-borderless"> | ||||
|                         <tbody> | ||||
|                           <tr> | ||||
|                             <td i18n>Date modified</td> | ||||
|                             <td>{{document.modified | customDate}}</td> | ||||
|                           </tr> | ||||
|                           <tr> | ||||
|                             <td i18n>Date added</td> | ||||
|                             <td>{{document.added | customDate}}</td> | ||||
|                           </tr> | ||||
|                           <tr> | ||||
|                             <td i18n>Media filename</td> | ||||
|                             <td>{{metadata?.media_filename}}</td> | ||||
|                           </tr> | ||||
|                           <tr> | ||||
|                             <td i18n>Original filename</td> | ||||
|                             <td>{{metadata?.original_filename}}</td> | ||||
|                           </tr> | ||||
|                           <tr> | ||||
|                             <td i18n>Original MD5 checksum</td> | ||||
|                             <td>{{metadata?.original_checksum}}</td> | ||||
|                           </tr> | ||||
|                           <tr> | ||||
|                             <td i18n>Original file size</td> | ||||
|                             <td>{{metadata?.original_size | fileSize}}</td> | ||||
|                           </tr> | ||||
|                           <tr> | ||||
|                             <td i18n>Original mime type</td> | ||||
|                             <td>{{metadata?.original_mime_type}}</td> | ||||
|                           </tr> | ||||
|                           @if (metadata?.has_archive_version) { | ||||
|                             <tr> | ||||
|                               <td i18n>Archive MD5 checksum</td> | ||||
|                               <td>{{metadata?.archive_checksum}}</td> | ||||
|                             </tr> | ||||
|                           } | ||||
|                           @if (metadata?.has_archive_version) { | ||||
|                             <tr> | ||||
|                               <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> | ||||
|                         <pngx-metadata-collapse i18n-title title="Archived document metadata" [metadata]="metadata.archive_metadata" *ngIf="metadata?.archive_metadata?.length > 0"></pngx-metadata-collapse> | ||||
|                     @if (metadata?.original_metadata?.length > 0) { | ||||
|                       <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 [ngbNavItem]="DocumentDetailNavIDs.Preview" class="d-md-none"> | ||||
|                     <a ngbNavLink i18n>Preview</a> | ||||
|                     <ng-template ngbNavContent *ngIf="!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> | ||||
|                   <a ngbNavLink i18n>Preview</a> | ||||
|                   @if (!pdfPreview.offsetParent) { | ||||
|                     <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> | ||||
|                   } | ||||
|                 </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> | ||||
|                     <ng-template ngbNavContent> | ||||
|                         <div class="mb-3"> | ||||
|                             <pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form> | ||||
|                         </div> | ||||
|                       <div class="mb-3"> | ||||
|                         <pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form> | ||||
|                       </div> | ||||
|                     </ng-template> | ||||
|                 </li> | ||||
|             </ul> | ||||
|                   </li> | ||||
|                 } | ||||
|               </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> | ||||
|           </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> | ||||
|         <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 & 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 & 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> | ||||
|     <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> | ||||
|             <button *ngIf="hasNext()" type="button" class="order-1 btn btn-sm btn-outline-primary" (click)="saveEditNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save & next</button> | ||||
|             <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 & 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 #previewContent> | ||||
|      <div *ngIf="!metadata" class="w-100 h-100 d-flex align-items-center justify-content-center"> | ||||
|          <div> | ||||
|              <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|              <ng-container i18n>Loading...</ng-container> | ||||
|          </div> | ||||
|      </div> | ||||
|      <ng-container *ngIf="getContentType() === 'application/pdf'"> | ||||
|          <div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer"> | ||||
|             <pngx-pdf-viewer | ||||
|                 [src]="{ url: previewUrl, password: password }" | ||||
|                 [original-size]="false" | ||||
|                 [show-borders]="true" | ||||
|                 [show-all]="true" | ||||
|                 [(page)]="previewCurrentPage" | ||||
|                 [zoom-scale]="previewZoomScale" | ||||
|                 [zoom]="previewZoomSetting" | ||||
|                 (error)="onError($event)" | ||||
|                 (after-load-complete)="pdfPreviewLoaded($event)"> | ||||
|             </pngx-pdf-viewer> | ||||
|          </div> | ||||
|          <ng-template #nativePdfViewer> | ||||
|              <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object> | ||||
|          </ng-template> | ||||
|      </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="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> | ||||
|         <ng-template #previewContent> | ||||
|           @if (!metadata) { | ||||
|             <div class="w-100 h-100 d-flex align-items-center justify-content-center"> | ||||
|               <div> | ||||
|                 <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|                 <ng-container i18n>Loading...</ng-container> | ||||
|               </div> | ||||
|             </div> | ||||
|           } | ||||
|           @if (getContentType() === 'application/pdf') { | ||||
|             @if (!useNativePdfViewer ) { | ||||
|               <div class="preview-sticky pdf-viewer-container"> | ||||
|                 <pngx-pdf-viewer | ||||
|                   [src]="{ url: previewUrl, password: password }" | ||||
|                   [original-size]="false" | ||||
|                   [show-borders]="true" | ||||
|                   [show-all]="true" | ||||
|                   [(page)]="previewCurrentPage" | ||||
|                   [zoom-scale]="previewZoomScale" | ||||
|                   [zoom]="previewZoomSetting" | ||||
|                   (error)="onError($event)" | ||||
|                   (after-load-complete)="pdfPreviewLoaded($event)"> | ||||
|                 </pngx-pdf-viewer> | ||||
|               </div> | ||||
|             } @else { | ||||
|               <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object> | ||||
|             } | ||||
|           } | ||||
|           @if (renderAsPlainText) { | ||||
|             <div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div> | ||||
|           } | ||||
|           @if (showPasswordField) { | ||||
|             <div 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> | ||||
|   | ||||
| @@ -1,23 +1,29 @@ | ||||
| <h6> | ||||
|   <button type="button" class="btn btn-outline-secondary btn-sm me-2" | ||||
|       (click)="expand = !expand"> | ||||
|       <svg class="buttonicon" fill="currentColor" *ngIf="!expand"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#caret-down" /> | ||||
|     (click)="expand = !expand"> | ||||
|     @if (!expand) { | ||||
|       <svg class="buttonicon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#caret-down" /> | ||||
|       </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> | ||||
|     } | ||||
|   </button> | ||||
|   {{title}} | ||||
| </h6> | ||||
|  | ||||
| <div #collapse="ngbCollapse" [(ngbCollapse)]="!expand"> | ||||
|   <table class="table table-borderless"> | ||||
|       <tbody> | ||||
|           <tr *ngFor="let m of metadata"> | ||||
|               <td>{{m.prefix}}:{{m.key}}</td> | ||||
|               <td class="metadata-column">{{m.value}}</td> | ||||
|           </tr> | ||||
|       </tbody> | ||||
|     <tbody> | ||||
|       @for (m of metadata; track m) { | ||||
|         <tr> | ||||
|           <td>{{m.prefix}}:{{m.key}}</td> | ||||
|           <td class="metadata-column">{{m.value}}</td> | ||||
|         </tr> | ||||
|       } | ||||
|     </tbody> | ||||
|   </table> | ||||
| </div> | ||||
|   | ||||
| @@ -3,140 +3,144 @@ | ||||
|     <button class="btn btn-sm btn-outline-secondary" (click)="list.selectNone()"> | ||||
|       <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#slash-circle" /> | ||||
|       </svg> <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> <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> <ng-container i18n>All</ng-container> | ||||
|         </svg> <ng-container i18n>Cancel</ng-container> | ||||
|       </button> | ||||
|     </div> | ||||
|   </div> | ||||
|     <div class="d-flex align-items-center gap-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"> | ||||
|       <label class="me-2" i18n>Edit:</label> | ||||
|       <pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title | ||||
|         filterPlaceholder="Filter tags" i18n-filterPlaceholder | ||||
|         [items]="tags" | ||||
|         [disabled]="!userCanEditAll" | ||||
|         [editing]="true" | ||||
|         [manyToOne]="true" | ||||
|         [applyOnClose]="applyOnClose" | ||||
|         (opened)="openTagsDropdown()" | ||||
|         [(selectionModel)]="tagSelectionModel" | ||||
|         [documentCounts]="tagDocumentCounts" | ||||
|         (apply)="setTags($event)"> | ||||
|       </pngx-filterable-dropdown> | ||||
|       <pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title | ||||
|         filterPlaceholder="Filter correspondents" i18n-filterPlaceholder | ||||
|         [items]="correspondents" | ||||
|         [disabled]="!userCanEditAll" | ||||
|         [editing]="true" | ||||
|         [applyOnClose]="applyOnClose" | ||||
|         (opened)="openCorrespondentDropdown()" | ||||
|         [(selectionModel)]="correspondentSelectionModel" | ||||
|         [documentCounts]="correspondentDocumentCounts" | ||||
|         (apply)="setCorrespondents($event)"> | ||||
|       </pngx-filterable-dropdown> | ||||
|       <pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title | ||||
|         filterPlaceholder="Filter document types" i18n-filterPlaceholder | ||||
|         [items]="documentTypes" | ||||
|         [disabled]="!userCanEditAll" | ||||
|         [editing]="true" | ||||
|         [applyOnClose]="applyOnClose" | ||||
|         (opened)="openDocumentTypeDropdown()" | ||||
|         [(selectionModel)]="documentTypeSelectionModel" | ||||
|         [documentCounts]="documentTypeDocumentCounts" | ||||
|         (apply)="setDocumentTypes($event)"> | ||||
|       </pngx-filterable-dropdown> | ||||
|       <pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title | ||||
|         filterPlaceholder="Filter storage paths" i18n-filterPlaceholder | ||||
|         [items]="storagePaths" | ||||
|         [disabled]="!userCanEditAll" | ||||
|         [editing]="true" | ||||
|         [applyOnClose]="applyOnClose" | ||||
|         (opened)="openStoragePathDropdown()" | ||||
|         [(selectionModel)]="storagePathsSelectionModel" | ||||
|         [documentCounts]="storagePathDocumentCounts" | ||||
|         (apply)="setStoragePaths($event)"> | ||||
|       </pngx-filterable-dropdown> | ||||
|   </div> | ||||
|   <div class="d-flex align-items-center gap-2 ms-auto"> | ||||
|     <div class="btn-toolbar"> | ||||
|  | ||||
|       <button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll"> | ||||
|         <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" /> | ||||
|         </svg><div class="d-none d-sm-inline"> <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"> <ng-container i18n>Actions</ng-container></div> | ||||
|         </button> | ||||
|         <div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow"> | ||||
|           <button ngbDropdownItem (click)="redoOcrSelected()" [disabled]="!userCanEditAll" i18n>Redo OCR</button> | ||||
|     <div class="d-flex align-items-center gap-2" role="group" aria-label="Select"> | ||||
|       <label class="me-2" i18n>Select:</label> | ||||
|       <div class="btn-group"> | ||||
|         <button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()"> | ||||
|           <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#file-earmark-check" /> | ||||
|             </svg> <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> <ng-container i18n>All</ng-container> | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-group btn-group-sm"> | ||||
|       <button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()"> | ||||
|         <svg *ngIf="!awaitingDownload" class="toolbaricon" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#arrow-down" /> | ||||
|         </svg> | ||||
|         <div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status"> | ||||
|           <span class="visually-hidden">Preparing download...</span> | ||||
|         <div class="d-flex align-items-center gap-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"> | ||||
|           <label class="me-2" i18n>Edit:</label> | ||||
|           <pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title | ||||
|             filterPlaceholder="Filter tags" i18n-filterPlaceholder | ||||
|             [items]="tags" | ||||
|             [disabled]="!userCanEditAll" | ||||
|             [editing]="true" | ||||
|             [manyToOne]="true" | ||||
|             [applyOnClose]="applyOnClose" | ||||
|             (opened)="openTagsDropdown()" | ||||
|             [(selectionModel)]="tagSelectionModel" | ||||
|             [documentCounts]="tagDocumentCounts" | ||||
|             (apply)="setTags($event)"> | ||||
|           </pngx-filterable-dropdown> | ||||
|           <pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title | ||||
|             filterPlaceholder="Filter correspondents" i18n-filterPlaceholder | ||||
|             [items]="correspondents" | ||||
|             [disabled]="!userCanEditAll" | ||||
|             [editing]="true" | ||||
|             [applyOnClose]="applyOnClose" | ||||
|             (opened)="openCorrespondentDropdown()" | ||||
|             [(selectionModel)]="correspondentSelectionModel" | ||||
|             [documentCounts]="correspondentDocumentCounts" | ||||
|             (apply)="setCorrespondents($event)"> | ||||
|           </pngx-filterable-dropdown> | ||||
|           <pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title | ||||
|             filterPlaceholder="Filter document types" i18n-filterPlaceholder | ||||
|             [items]="documentTypes" | ||||
|             [disabled]="!userCanEditAll" | ||||
|             [editing]="true" | ||||
|             [applyOnClose]="applyOnClose" | ||||
|             (opened)="openDocumentTypeDropdown()" | ||||
|             [(selectionModel)]="documentTypeSelectionModel" | ||||
|             [documentCounts]="documentTypeDocumentCounts" | ||||
|             (apply)="setDocumentTypes($event)"> | ||||
|           </pngx-filterable-dropdown> | ||||
|           <pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title | ||||
|             filterPlaceholder="Filter storage paths" i18n-filterPlaceholder | ||||
|             [items]="storagePaths" | ||||
|             [disabled]="!userCanEditAll" | ||||
|             [editing]="true" | ||||
|             [applyOnClose]="applyOnClose" | ||||
|             (opened)="openStoragePathDropdown()" | ||||
|             [(selectionModel)]="storagePathsSelectionModel" | ||||
|             [documentCounts]="storagePathDocumentCounts" | ||||
|             (apply)="setStoragePaths($event)"> | ||||
|           </pngx-filterable-dropdown> | ||||
|         </div> | ||||
|         <div class="d-none d-sm-inline"> <ng-container i18n>Download</ng-container></div> | ||||
|       </button> | ||||
|       <div ngbDropdown class="me-2 d-flex btn-group" role="group"> | ||||
|         <button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle-split rounded-end" ngbDropdownToggle></button> | ||||
|         <div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow"> | ||||
| 					<form [formGroup]="downloadForm" class="px-3 py-1"> | ||||
|             <p class="mb-1" i18n>Include:</p> | ||||
|             <div class="form-group ps-3 mb-2"> | ||||
|               <div class="form-check"> | ||||
|                 <input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" /> | ||||
|                 <label class="form-check-label" for="downloadFileType_archive" i18n> | ||||
|         <div class="d-flex align-items-center gap-2 ms-auto"> | ||||
|           <div class="btn-toolbar"> | ||||
|  | ||||
|             <button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll"> | ||||
|               <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" /> | ||||
|                 </svg><div class="d-none d-sm-inline"> <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"> <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"> <ng-container i18n>Download</ng-container></div> | ||||
|               </button> | ||||
|               <div ngbDropdown class="me-2 d-flex btn-group" role="group"> | ||||
|                 <button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle-split rounded-end" ngbDropdownToggle></button> | ||||
|                 <div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow"> | ||||
|                   <form [formGroup]="downloadForm" class="px-3 py-1"> | ||||
|                     <p class="mb-1" i18n>Include:</p> | ||||
|                     <div class="form-group ps-3 mb-2"> | ||||
|                       <div class="form-check"> | ||||
|                         <input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" /> | ||||
|                         <label class="form-check-label" for="downloadFileType_archive" i18n> | ||||
|                   Archived files | ||||
|                 </label> | ||||
|               </div> | ||||
|               <div class="form-check"> | ||||
|                 <input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" /> | ||||
|                 <label class="form-check-label" for="downloadFileType_originals" i18n> | ||||
|                       </div> | ||||
|                       <div class="form-check"> | ||||
|                         <input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" /> | ||||
|                         <label class="form-check-label" for="downloadFileType_originals" i18n> | ||||
|                   Original files | ||||
|                 </label> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="form-check"> | ||||
|               <input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" /> | ||||
|               <label class="form-check-label" for="downloadUseFormatting" i18n> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                     <div class="form-check"> | ||||
|                       <input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" /> | ||||
|                       <label class="form-check-label" for="downloadUseFormatting" i18n> | ||||
|                 Use formatted filename | ||||
|               </label> | ||||
|                     </div> | ||||
|                   </form> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </form> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-group btn-group-sm"> | ||||
|       <button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll"> | ||||
|         <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|         </svg> <ng-container i18n>Delete</ng-container> | ||||
|       </button> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|             <div class="btn-group btn-group-sm"> | ||||
|               <button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll"> | ||||
|                 <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                   </svg> <ng-container i18n>Delete</ng-container> | ||||
|                 </button> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|   | ||||
| @@ -16,23 +16,35 @@ | ||||
|  | ||||
|         <div class="d-flex justify-content-between align-items-center"> | ||||
|           <h5 class="card-title"> | ||||
|             <ng-container *ngIf="document.correspondent"> | ||||
|               <a *ngIf="clickCorrespondent.observers.length ; else nolink" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a> | ||||
|               <ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>: | ||||
|             </ng-container> | ||||
|             @if (document.correspondent) { | ||||
|               @if (clickCorrespondent.observers.length ) { | ||||
|                 <a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a> | ||||
|               } @else { | ||||
|                 {{(document.correspondent$ | async)?.name}} | ||||
|               } | ||||
|               : | ||||
|             } | ||||
|             {{document.title | documentTitle}} | ||||
|             <pngx-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle *ngFor="let t of document.tags$ | async" class="ms-1" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="clickTag.observers.length"></pngx-tag> | ||||
|             @for (t of document.tags$ | async; track t) { | ||||
|               <pngx-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle class="ms-1" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="clickTag.observers.length"></pngx-tag> | ||||
|             } | ||||
|           </h5> | ||||
|         </div> | ||||
|         <p class="card-text"> | ||||
|           <span *ngIf="document.__search_hit__ && document.__search_hit__.highlights" [innerHtml]="document.__search_hit__.highlights"></span> | ||||
|           <span *ngFor="let highlight of searchNoteHighlights" class="d-block"> | ||||
|             <svg width="1em" height="1em" fill="currentColor" class="me-2"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> | ||||
|             </svg> | ||||
|             <span [innerHtml]="highlight"></span> | ||||
|           </span> | ||||
|           <span *ngIf="!document.__search_hit__" class="result-content">{{contentTrimmed}}</span> | ||||
|           @if (document.__search_hit__ && document.__search_hit__.highlights) { | ||||
|             <span [innerHtml]="document.__search_hit__.highlights"></span> | ||||
|           } | ||||
|           @for (highlight of searchNoteHighlights; track highlight) { | ||||
|             <span class="d-block"> | ||||
|               <svg width="1em" height="1em" fill="currentColor" class="me-2"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> | ||||
|               </svg> | ||||
|               <span [innerHtml]="highlight"></span> | ||||
|             </span> | ||||
|           } | ||||
|           @if (!document.__search_hit__) { | ||||
|             <span class="result-content">{{contentTrimmed}}</span> | ||||
|           } | ||||
|         </p> | ||||
|  | ||||
|  | ||||
| @@ -41,91 +53,105 @@ | ||||
|             <a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()"> | ||||
|               <svg class="sidebaricon" fill="currentColor" class="sidebaricon"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#diagram-3"/> | ||||
|               </svg> <span class="d-none d-md-inline" i18n>More like this</span> | ||||
|             </a> | ||||
|             <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"> | ||||
|               <svg class="sidebaricon" fill="currentColor" class="sidebaricon"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#pencil"/> | ||||
|               </svg> <span class="d-none d-md-inline" i18n>Edit</span> | ||||
|             </a> | ||||
|             <a class="btn btn-sm btn-outline-secondary" target="_blank" [href]="previewUrl" | ||||
|             [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" | ||||
|             autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> | ||||
|               <svg class="sidebaricon" fill="currentColor" class="sidebaricon"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#eye"/> | ||||
|               </svg> <span class="d-none d-md-inline" i18n>View</span> | ||||
|             </a> | ||||
|             <ng-template #previewContent> | ||||
|               <pngx-preview-popup [document]="document"></pngx-preview-popup> | ||||
|             </ng-template> | ||||
|             <a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()"> | ||||
|               <svg class="sidebaricon" fill="currentColor" class="sidebaricon"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#download"/> | ||||
|               </svg> <span class="d-none d-md-inline" i18n>Download</span> | ||||
|             </a> | ||||
|           </div> | ||||
|                 </svg> <span class="d-none d-md-inline" i18n>More like this</span> | ||||
|               </a> | ||||
|               <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"> | ||||
|                 <svg class="sidebaricon" fill="currentColor" class="sidebaricon"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#pencil"/> | ||||
|                   </svg> <span class="d-none d-md-inline" i18n>Edit</span> | ||||
|                 </a> | ||||
|                 <a class="btn btn-sm btn-outline-secondary" target="_blank" [href]="previewUrl" | ||||
|                   [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" | ||||
|                   autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> | ||||
|                   <svg class="sidebaricon" fill="currentColor" class="sidebaricon"> | ||||
|                     <use xlink:href="assets/bootstrap-icons.svg#eye"/> | ||||
|                     </svg> <span class="d-none d-md-inline" i18n>View</span> | ||||
|                   </a> | ||||
|                   <ng-template #previewContent> | ||||
|                     <pngx-preview-popup [document]="document"></pngx-preview-popup> | ||||
|                   </ng-template> | ||||
|                   <a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()"> | ||||
|                     <svg class="sidebaricon" fill="currentColor" class="sidebaricon"> | ||||
|                       <use xlink:href="assets/bootstrap-icons.svg#download"/> | ||||
|                       </svg> <span class="d-none d-md-inline" i18n>Download</span> | ||||
|                     </a> | ||||
|                   </div> | ||||
|  | ||||
|  | ||||
|           <div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0"> | ||||
|             <button *ngIf="notesEnabled && document.notes.length" routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="View notes" i18n-title> | ||||
|               <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> | ||||
|               </svg> | ||||
|               <small i18n>{{document.notes.length}} Notes</small> | ||||
|             </button> | ||||
|             <button *ngIf="document.document_type" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by document type" i18n-title | ||||
|              (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> | ||||
|               <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#file-earmark"/> | ||||
|               </svg> | ||||
|               <small>{{(document.document_type$ | async)?.name}}</small> | ||||
|             </button> | ||||
|             <button *ngIf="document.storage_path" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by storage path" i18n-title | ||||
|              (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()"> | ||||
|               <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#archive"/> | ||||
|               </svg> | ||||
|               <small>{{(document.storage_path$ | async)?.name}}</small> | ||||
|             </button> | ||||
|             <div *ngIf="document.archive_serial_number | isNumber" class="list-group-item me-2 bg-light text-dark p-1 border-0"> | ||||
|               <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#upc-scan"/> | ||||
|               </svg> | ||||
|               <small>#{{document.archive_serial_number}}</small> | ||||
|             </div> | ||||
|             <ng-template #dateTooltip> | ||||
|               <div class="d-flex flex-column text-light"> | ||||
|                 <span i18n>Created: {{ document.created | customDate }}</span> | ||||
|                 <span i18n>Added: {{ document.added | customDate }}</span> | ||||
|                 <span i18n>Modified: {{ document.modified | customDate }}</span> | ||||
|                   <div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0"> | ||||
|                     @if (notesEnabled && document.notes.length) { | ||||
|                       <button routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="View notes" i18n-title> | ||||
|                         <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                           <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> | ||||
|                         </svg> | ||||
|                         <small i18n>{{document.notes.length}} Notes</small> | ||||
|                       </button> | ||||
|                     } | ||||
|                     @if (document.document_type) { | ||||
|                       <button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by document type" i18n-title | ||||
|                         (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> | ||||
|                         <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                           <use xlink:href="assets/bootstrap-icons.svg#file-earmark"/> | ||||
|                         </svg> | ||||
|                         <small>{{(document.document_type$ | async)?.name}}</small> | ||||
|                       </button> | ||||
|                     } | ||||
|                     @if (document.storage_path) { | ||||
|                       <button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by storage path" i18n-title | ||||
|                         (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()"> | ||||
|                         <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                           <use xlink:href="assets/bootstrap-icons.svg#archive"/> | ||||
|                         </svg> | ||||
|                         <small>{{(document.storage_path$ | async)?.name}}</small> | ||||
|                       </button> | ||||
|                     } | ||||
|                     @if (document.archive_serial_number | isNumber) { | ||||
|                       <div class="list-group-item me-2 bg-light text-dark p-1 border-0"> | ||||
|                         <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                           <use xlink:href="assets/bootstrap-icons.svg#upc-scan"/> | ||||
|                         </svg> | ||||
|                         <small>#{{document.archive_serial_number}}</small> | ||||
|                       </div> | ||||
|                     } | ||||
|                     <ng-template #dateTooltip> | ||||
|                       <div class="d-flex flex-column text-light"> | ||||
|                         <span i18n>Created: {{ document.created | customDate }}</span> | ||||
|                         <span i18n>Added: {{ document.added | customDate }}</span> | ||||
|                         <span i18n>Modified: {{ document.modified | customDate }}</span> | ||||
|                       </div> | ||||
|                     </ng-template> | ||||
|                     <div class="list-group-item bg-light text-dark p-1 border-0" [ngbTooltip]="dateTooltip"> | ||||
|                       <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#calendar-event"/> | ||||
|                       </svg> | ||||
|                       <small>{{document.created_date | customDate:'mediumDate'}}</small> | ||||
|                     </div> | ||||
|                     @if (document.owner && document.owner !== settingsService.currentUser.id) { | ||||
|                       <div class="list-group-item bg-light text-dark p-1 border-0"> | ||||
|                         <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                           <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/> | ||||
|                         </svg> | ||||
|                         <small>{{document.owner | username}}</small> | ||||
|                       </div> | ||||
|                     } | ||||
|                     @if (document.is_shared_by_requester) { | ||||
|                       <div class="list-group-item bg-light text-dark p-1 border-0"> | ||||
|                         <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                           <use xlink:href="assets/bootstrap-icons.svg#people-fill"/> | ||||
|                         </svg> | ||||
|                         <small i18n>Shared</small> | ||||
|                       </div> | ||||
|                     } | ||||
|                     @if (document.__search_hit__?.score) { | ||||
|                       <div class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score"> | ||||
|                         <small class="text-muted" i18n>Score:</small> | ||||
|                         <ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar> | ||||
|                       </div> | ||||
|                     } | ||||
|                   </div> | ||||
|                 </div> | ||||
|  | ||||
|               </div> | ||||
|             </ng-template> | ||||
|             <div class="list-group-item bg-light text-dark p-1 border-0" [ngbTooltip]="dateTooltip"> | ||||
|               <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#calendar-event"/> | ||||
|               </svg> | ||||
|               <small>{{document.created_date | customDate:'mediumDate'}}</small> | ||||
|             </div> | ||||
|             <div *ngIf="document.owner && document.owner !== settingsService.currentUser.id" class="list-group-item bg-light text-dark p-1 border-0"> | ||||
|               <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/> | ||||
|               </svg> | ||||
|               <small>{{document.owner | username}}</small> | ||||
|             </div> | ||||
|             <div *ngIf="document.is_shared_by_requester" class="list-group-item bg-light text-dark p-1 border-0"> | ||||
|               <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#people-fill"/> | ||||
|               </svg> | ||||
|               <small i18n>Shared</small> | ||||
|             </div> | ||||
|             <div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score"> | ||||
|               <small class="text-muted" i18n>Score:</small> | ||||
|               <ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -11,45 +11,55 @@ | ||||
|       </div> | ||||
|  | ||||
|       <div class="tags d-flex flex-column text-end position-absolute me-1 fs-6"> | ||||
|         <pngx-tag *ngFor="let t of getTagsLimited$() | async" [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag> | ||||
|         <div *ngIf="moreTags"> | ||||
|           <span class="badge text-dark">+ {{moreTags}}</span> | ||||
|         </div> | ||||
|         @for (t of getTagsLimited$() | async; track t) { | ||||
|           <pngx-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag> | ||||
|         } | ||||
|         @if (moreTags) { | ||||
|           <div> | ||||
|             <span class="badge text-dark">+ {{moreTags}}</span> | ||||
|           </div> | ||||
|         } | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <a *ngIf="notesEnabled && document.notes.length" routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1"> | ||||
|       <span class="badge rounded-pill bg-light border text-primary"> | ||||
|       <svg class="metadata-icon ms-1 me-1" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> | ||||
|       </svg> | ||||
|       {{document.notes.length}}</span> | ||||
|     </a> | ||||
|     @if (notesEnabled && document.notes.length) { | ||||
|       <a routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1"> | ||||
|         <span class="badge rounded-pill bg-light border text-primary"> | ||||
|           <svg class="metadata-icon ms-1 me-1" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> | ||||
|           </svg> | ||||
|         {{document.notes.length}}</span> | ||||
|       </a> | ||||
|     } | ||||
|  | ||||
|     <div class="card-body bg-light p-2"> | ||||
|       <p class="card-text"> | ||||
|         <ng-container *ngIf="document.correspondent"> | ||||
|         @if (document.correspondent) { | ||||
|           <a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name ?? privateName}}</a>: | ||||
|         </ng-container> | ||||
|         } | ||||
|         {{document.title | documentTitle}} | ||||
|       </p> | ||||
|     </div> | ||||
|     <div class="card-footer pt-0 pb-2 px-2"> | ||||
|       <div class="list-group list-group-flush border-0 pt-1 pb-2 card-info"> | ||||
|         <button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title | ||||
|          (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> | ||||
|           <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#file-earmark"/> | ||||
|           </svg> | ||||
|           <small>{{(document.document_type$ | async)?.name ?? privateName}}</small> | ||||
|         </button> | ||||
|         <button *ngIf="document.storage_path" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title | ||||
|          (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()"> | ||||
|           <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#folder"/> | ||||
|           </svg> | ||||
|           <small>{{(document.storage_path$ | async)?.name ?? privateName}}</small> | ||||
|         </button> | ||||
|         @if (document.document_type) { | ||||
|           <button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title | ||||
|             (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> | ||||
|             <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#file-earmark"/> | ||||
|             </svg> | ||||
|             <small>{{(document.document_type$ | async)?.name ?? privateName}}</small> | ||||
|           </button> | ||||
|         } | ||||
|         @if (document.storage_path) { | ||||
|           <button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title | ||||
|             (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()"> | ||||
|             <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#folder"/> | ||||
|             </svg> | ||||
|             <small>{{(document.storage_path$ | async)?.name ?? privateName}}</small> | ||||
|           </button> | ||||
|         } | ||||
|         <div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between"> | ||||
|           <ng-template #dateTooltip> | ||||
|             <div class="d-flex flex-column text-light"> | ||||
| @@ -65,24 +75,30 @@ | ||||
|             <small>{{document.created_date | customDate:'mediumDate'}}</small> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div *ngIf="document.archive_serial_number | isNumber" class="ps-0 p-1"> | ||||
|           <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#upc-scan"/> | ||||
|           </svg> | ||||
|           <small>#{{document.archive_serial_number}}</small> | ||||
|         </div> | ||||
|         <div *ngIf="document.owner && document.owner !== settingsService.currentUser.id" class="ps-0 p-1"> | ||||
|           <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/> | ||||
|           </svg> | ||||
|           <small>{{document.owner | username}}</small> | ||||
|         </div> | ||||
|         <div *ngIf="document.is_shared_by_requester" class="ps-0 p-1"> | ||||
|           <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#people-fill"/> | ||||
|           </svg> | ||||
|           <small i18n>Shared</small> | ||||
|         </div> | ||||
|         @if (document.archive_serial_number | isNumber) { | ||||
|           <div class="ps-0 p-1"> | ||||
|             <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#upc-scan"/> | ||||
|             </svg> | ||||
|             <small>#{{document.archive_serial_number}}</small> | ||||
|           </div> | ||||
|         } | ||||
|         @if (document.owner && document.owner !== settingsService.currentUser.id) { | ||||
|           <div class="ps-0 p-1"> | ||||
|             <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/> | ||||
|             </svg> | ||||
|             <small>{{document.owner | username}}</small> | ||||
|           </div> | ||||
|         } | ||||
|         @if (document.is_shared_by_requester) { | ||||
|           <div class="ps-0 p-1"> | ||||
|             <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#people-fill"/> | ||||
|             </svg> | ||||
|             <small i18n>Shared</small> | ||||
|           </div> | ||||
|         } | ||||
|       </div> | ||||
|       <div class="d-flex justify-content-between align-items-center"> | ||||
|         <div class="btn-group w-100"> | ||||
| @@ -92,8 +108,8 @@ | ||||
|             </svg> | ||||
|           </a> | ||||
|           <a [href]="previewUrl" target="_blank" class="btn btn-sm btn-outline-secondary" | ||||
|           [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" | ||||
|           autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> | ||||
|             [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" | ||||
|             autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> | ||||
|             <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16"> | ||||
|               <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/> | ||||
|               <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/> | ||||
|   | ||||
| @@ -52,9 +52,11 @@ | ||||
|         </label> | ||||
|       </div> | ||||
|       <div> | ||||
|         <button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSortField(f.field)" | ||||
|           [class.active]="list.sortField === f.field">{{f.name}} | ||||
|         </button> | ||||
|         @for (f of getSortFields(); track f) { | ||||
|           <button ngbDropdownItem (click)="setSortField(f.field)" | ||||
|             [class.active]="list.sortField === f.field">{{f.name}} | ||||
|           </button> | ||||
|         } | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| @@ -62,18 +64,26 @@ | ||||
|   <div class="btn-group ms-2 flex-fill" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }" ngbDropdown role="group"> | ||||
|     <button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle> | ||||
|       <ng-container i18n>Views</ng-container> | ||||
|       <div *ngIf="savedViewIsModified" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle"> | ||||
|         <span class="visually-hidden">selected</span> | ||||
|       </div> | ||||
|       @if (savedViewIsModified) { | ||||
|         <div class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle"> | ||||
|           <span class="visually-hidden">selected</span> | ||||
|         </div> | ||||
|       } | ||||
|     </button> | ||||
|     <div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu> | ||||
|       <ng-container *ngIf="!list.activeSavedViewId"> | ||||
|         <button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view.id)">{{view.name}}</button> | ||||
|         <div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div> | ||||
|       </ng-container> | ||||
|       @if (!list.activeSavedViewId) { | ||||
|         @for (view of savedViewService.allViews; track view) { | ||||
|           <button ngbDropdownItem (click)="loadViewConfig(view.id)">{{view.name}}</button> | ||||
|         } | ||||
|         @if (savedViewService.allViews.length > 0) { | ||||
|           <div class="dropdown-divider"></div> | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       <div *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.SavedView }"> | ||||
|         <button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button> | ||||
|         @if (list.activeSavedViewId) { | ||||
|           <button ngbDropdownItem (click)="saveViewConfig()" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button> | ||||
|         } | ||||
|       </div> | ||||
|       <button ngbDropdownItem (click)="saveViewConfigAs()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.SavedView }" i18n>Save as...</button> | ||||
|     </div> | ||||
| @@ -90,165 +100,191 @@ | ||||
| <ng-template #pagination> | ||||
|   <div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3"> | ||||
|     <div class="d-flex align-items-center"> | ||||
|       <ng-container *ngIf="list.isReloading"> | ||||
|       @if (list.isReloading) { | ||||
|         <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|         <ng-container i18n>Loading...</ng-container> | ||||
|       </ng-container> | ||||
|       <span i18n *ngIf="list.selected.size > 0">{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span> | ||||
|       <ng-container *ngIf="!list.isReloading"> | ||||
|         <span i18n *ngIf="list.selected.size === 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> <span i18n *ngIf="isFiltered">(filtered)</span> | ||||
|       </ng-container> | ||||
|       <button *ngIf="!list.isReloading && isFiltered" class="btn btn-link py-0" (click)="resetFilters()"> | ||||
|         <svg fill="currentColor" class="buttonicon-sm"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|         </svg><small i18n>Reset filters</small> | ||||
|       </button> | ||||
|       } | ||||
|       @if (list.selected.size > 0) { | ||||
|         <span i18n>{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span> | ||||
|       } | ||||
|       @if (!list.isReloading) { | ||||
|         @if (list.selected.size === 0) { | ||||
|           <span i18n>{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> | ||||
|           } @if (isFiltered) { | ||||
|           <span i18n>(filtered)</span> | ||||
|         } | ||||
|       } | ||||
|       @if (!list.isReloading && isFiltered) { | ||||
|         <button class="btn btn-link py-0" (click)="resetFilters()"> | ||||
|           <svg fill="currentColor" class="buttonicon-sm"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|             </svg><small i18n>Reset filters</small> | ||||
|           </button> | ||||
|         } | ||||
|       </div> | ||||
|       @if (list.collectionSize) { | ||||
|         <ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" | ||||
|         [rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination> | ||||
|       } | ||||
|     </div> | ||||
|     <ngb-pagination *ngIf="list.collectionSize" [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" | ||||
|     [rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination> | ||||
|   </div> | ||||
| </ng-template> | ||||
|   </ng-template> | ||||
|  | ||||
| <div tourAnchor="tour.documents"> | ||||
|   <ng-container *ngTemplateOutlet="pagination"></ng-container> | ||||
| </div> | ||||
|  | ||||
| <ng-container *ngIf="list.error ; else documentListNoError"> | ||||
|   <div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div> | ||||
| </ng-container> | ||||
|  | ||||
| <ng-template #documentListNoError> | ||||
|   <div *ngIf="displayMode === 'largeCards'"> | ||||
|     <pngx-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickStoragePath)="clickStoragePath($event)" (clickMoreLike)="clickMoreLike(d.id)"> | ||||
|     </pngx-document-card-large> | ||||
|   </div> | ||||
|  | ||||
|   <table class="table table-sm align-middle border shadow-sm" *ngIf="displayMode === 'details'"> | ||||
|     <thead> | ||||
|       <th></th> | ||||
|       <th class="d-none d-lg-table-cell" | ||||
|         pngxSortable="archive_serial_number" | ||||
|         title="Sort by ASN" i18n-title | ||||
|         [currentSortField]="list.sortField" | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>ASN</th> | ||||
|       <th class="d-none d-md-table-cell" | ||||
|         pngxSortable="correspondent__name" | ||||
|         title="Sort by correspondent" i18n-title | ||||
|         [currentSortField]="list.sortField" | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>Correspondent</th> | ||||
|       <th | ||||
|         pngxSortable="title" | ||||
|         title="Sort by title" i18n-title | ||||
|         class="w-40" | ||||
|         [currentSortField]="list.sortField" | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>Title</th> | ||||
|       <th class="d-none d-xl-table-cell" | ||||
|         pngxSortable="owner" | ||||
|         title="Sort by owner" i18n-title | ||||
|         [currentSortField]="list.sortField" | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>Owner</th> | ||||
|       <th *ngIf="notesEnabled" class="d-none d-xl-table-cell" | ||||
|         pngxSortable="num_notes" | ||||
|         title="Sort by notes" i18n-title | ||||
|         [currentSortField]="list.sortField" | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>Notes</th> | ||||
|       <th class="d-none d-xl-table-cell" | ||||
|         pngxSortable="document_type__name" | ||||
|         title="Sort by document type" i18n-title | ||||
|         [currentSortField]="list.sortField" | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>Document type</th> | ||||
|       <th class="d-none d-xl-table-cell" | ||||
|         pngxSortable="storage_path__name" | ||||
|         title="Sort by storage path" i18n-title | ||||
|         [currentSortField]="list.sortField" | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>Storage path</th> | ||||
|       <th | ||||
|         pngxSortable="created" | ||||
|         title="Sort by created date" i18n-title | ||||
|         [currentSortField]="list.sortField" | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>Created</th> | ||||
|       <th class="d-none d-xl-table-cell" | ||||
|         pngxSortable="added" | ||||
|         title="Sort by added date" i18n-title | ||||
|         [currentSortField]="list.sortField" | ||||
|         [currentSortReverse]="list.sortReverse" | ||||
|         (sort)="onSort($event)" | ||||
|         i18n>Added</th> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|       <tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" (click)="toggleSelected(d, $event); $event.stopPropagation();" (dblclick)="openDocumentDetail(d)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''"> | ||||
|         <td> | ||||
|           <div class="form-check"> | ||||
|             <input type="checkbox" class="form-check-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event); $event.stopPropagation();"> | ||||
|             <label class="form-check-label" for="docCheck{{d.id}}"></label> | ||||
|           </div> | ||||
|         </td> | ||||
|         <td class="d-none d-lg-table-cell"> | ||||
|           {{d.archive_serial_number}} | ||||
|         </td> | ||||
|         <td class="d-none d-md-table-cell"> | ||||
|           <ng-container *ngIf="d.correspondent"> | ||||
|             <a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a> | ||||
|           </ng-container> | ||||
|         </td> | ||||
|         <td> | ||||
|           <a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> | ||||
|           <pngx-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag> | ||||
|         </td> | ||||
|         <td> | ||||
|           {{d.owner | username}} | ||||
|         </td> | ||||
|         <td *ngIf="notesEnabled" class="d-none d-xl-table-cell"> | ||||
|           <a *ngIf="d.notes.length" routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0"> | ||||
|             <span class="badge rounded-pill bg-light border text-primary"> | ||||
|             <svg class="metadata-icon ms-1 me-1" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> | ||||
|             </svg> | ||||
|             {{d.notes.length}}</span> | ||||
|           </a> | ||||
|         </td> | ||||
|         <td class="d-none d-xl-table-cell"> | ||||
|           <ng-container *ngIf="d.document_type"> | ||||
|             <a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a> | ||||
|           </ng-container> | ||||
|         </td> | ||||
|         <td class="d-none d-xl-table-cell"> | ||||
|           <ng-container *ngIf="d.storage_path"> | ||||
|             <a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a> | ||||
|           </ng-container> | ||||
|         </td> | ||||
|         <td> | ||||
|           {{d.created_date | customDate}} | ||||
|         </td> | ||||
|         <td class="d-none d-xl-table-cell"> | ||||
|           {{d.added | customDate}} | ||||
|         </td> | ||||
|       </tr> | ||||
|     </tbody> | ||||
|   </table> | ||||
|  | ||||
|   <div class="row row-cols-paperless-cards" *ngIf="displayMode === 'smallCards'"> | ||||
|     <pngx-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></pngx-document-card-small> | ||||
|   </div> | ||||
|   <div *ngIf="list.documents?.length > 15" class="mt-3"> | ||||
|   <div tourAnchor="tour.documents"> | ||||
|     <ng-container *ngTemplateOutlet="pagination"></ng-container> | ||||
|   </div> | ||||
|  | ||||
|  | ||||
| </ng-template> | ||||
|   @if (list.error ) { | ||||
|     <div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div> | ||||
|   } @else { | ||||
|     @if (displayMode === 'largeCards') { | ||||
|       <div> | ||||
|         @for (d of list.documents; track trackByDocumentId($index, d)) { | ||||
|           <pngx-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickStoragePath)="clickStoragePath($event)" (clickMoreLike)="clickMoreLike(d.id)"> | ||||
|           </pngx-document-card-large> | ||||
|         } | ||||
|       </div> | ||||
|     } | ||||
|     @if (displayMode === 'details') { | ||||
|       <table class="table table-sm align-middle border shadow-sm"> | ||||
|         <thead> | ||||
|           <th></th> | ||||
|           <th class="d-none d-lg-table-cell" | ||||
|             pngxSortable="archive_serial_number" | ||||
|             title="Sort by ASN" i18n-title | ||||
|             [currentSortField]="list.sortField" | ||||
|             [currentSortReverse]="list.sortReverse" | ||||
|             (sort)="onSort($event)" | ||||
|           i18n>ASN</th> | ||||
|           <th class="d-none d-md-table-cell" | ||||
|             pngxSortable="correspondent__name" | ||||
|             title="Sort by correspondent" i18n-title | ||||
|             [currentSortField]="list.sortField" | ||||
|             [currentSortReverse]="list.sortReverse" | ||||
|             (sort)="onSort($event)" | ||||
|           i18n>Correspondent</th> | ||||
|           <th | ||||
|             pngxSortable="title" | ||||
|             title="Sort by title" i18n-title | ||||
|             class="w-40" | ||||
|             [currentSortField]="list.sortField" | ||||
|             [currentSortReverse]="list.sortReverse" | ||||
|             (sort)="onSort($event)" | ||||
|           i18n>Title</th> | ||||
|           <th class="d-none d-xl-table-cell" | ||||
|             pngxSortable="owner" | ||||
|             title="Sort by owner" i18n-title | ||||
|             [currentSortField]="list.sortField" | ||||
|             [currentSortReverse]="list.sortReverse" | ||||
|             (sort)="onSort($event)" | ||||
|           i18n>Owner</th> | ||||
|           @if (notesEnabled) { | ||||
|             <th class="d-none d-xl-table-cell" | ||||
|               pngxSortable="num_notes" | ||||
|               title="Sort by notes" i18n-title | ||||
|               [currentSortField]="list.sortField" | ||||
|               [currentSortReverse]="list.sortReverse" | ||||
|               (sort)="onSort($event)" | ||||
|             i18n>Notes</th> | ||||
|           } | ||||
|           <th class="d-none d-xl-table-cell" | ||||
|             pngxSortable="document_type__name" | ||||
|             title="Sort by document type" i18n-title | ||||
|             [currentSortField]="list.sortField" | ||||
|             [currentSortReverse]="list.sortReverse" | ||||
|             (sort)="onSort($event)" | ||||
|           i18n>Document type</th> | ||||
|           <th class="d-none d-xl-table-cell" | ||||
|             pngxSortable="storage_path__name" | ||||
|             title="Sort by storage path" i18n-title | ||||
|             [currentSortField]="list.sortField" | ||||
|             [currentSortReverse]="list.sortReverse" | ||||
|             (sort)="onSort($event)" | ||||
|           i18n>Storage path</th> | ||||
|           <th | ||||
|             pngxSortable="created" | ||||
|             title="Sort by created date" i18n-title | ||||
|             [currentSortField]="list.sortField" | ||||
|             [currentSortReverse]="list.sortReverse" | ||||
|             (sort)="onSort($event)" | ||||
|           i18n>Created</th> | ||||
|           <th class="d-none d-xl-table-cell" | ||||
|             pngxSortable="added" | ||||
|             title="Sort by added date" i18n-title | ||||
|             [currentSortField]="list.sortField" | ||||
|             [currentSortReverse]="list.sortReverse" | ||||
|             (sort)="onSort($event)" | ||||
|           i18n>Added</th> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           @for (d of list.documents; track trackByDocumentId($index, d)) { | ||||
|             <tr (click)="toggleSelected(d, $event); $event.stopPropagation();" (dblclick)="openDocumentDetail(d)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''"> | ||||
|               <td> | ||||
|                 <div class="form-check"> | ||||
|                   <input type="checkbox" class="form-check-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event); $event.stopPropagation();"> | ||||
|                   <label class="form-check-label" for="docCheck{{d.id}}"></label> | ||||
|                 </div> | ||||
|               </td> | ||||
|               <td class="d-none d-lg-table-cell"> | ||||
|                 {{d.archive_serial_number}} | ||||
|               </td> | ||||
|               <td class="d-none d-md-table-cell"> | ||||
|                 @if (d.correspondent) { | ||||
|                   <a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a> | ||||
|                 } | ||||
|               </td> | ||||
|               <td> | ||||
|                 <a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> | ||||
|                 @for (t of d.tags$ | async; track t) { | ||||
|                   <pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag> | ||||
|                 } | ||||
|               </td> | ||||
|               <td> | ||||
|                 {{d.owner | username}} | ||||
|               </td> | ||||
|               @if (notesEnabled) { | ||||
|                 <td class="d-none d-xl-table-cell"> | ||||
|                   @if (d.notes.length) { | ||||
|                     <a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0"> | ||||
|                       <span class="badge rounded-pill bg-light border text-primary"> | ||||
|                         <svg class="metadata-icon ms-1 me-1" fill="currentColor"> | ||||
|                           <use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/> | ||||
|                         </svg> | ||||
|                       {{d.notes.length}}</span> | ||||
|                     </a> | ||||
|                   } | ||||
|                 </td> | ||||
|               } | ||||
|               <td class="d-none d-xl-table-cell"> | ||||
|                 @if (d.document_type) { | ||||
|                   <a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a> | ||||
|                 } | ||||
|               </td> | ||||
|               <td class="d-none d-xl-table-cell"> | ||||
|                 @if (d.storage_path) { | ||||
|                   <a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a> | ||||
|                 } | ||||
|               </td> | ||||
|               <td> | ||||
|                 {{d.created_date | customDate}} | ||||
|               </td> | ||||
|               <td class="d-none d-xl-table-cell"> | ||||
|                 {{d.added | customDate}} | ||||
|               </td> | ||||
|             </tr> | ||||
|           } | ||||
|         </tbody> | ||||
|       </table> | ||||
|     } | ||||
|     @if (displayMode === 'smallCards') { | ||||
|       <div class="row row-cols-paperless-cards"> | ||||
|         @for (d of list.documents; track trackByDocumentId($index, d)) { | ||||
|           <pngx-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></pngx-document-card-small> | ||||
|         } | ||||
|       </div> | ||||
|     } | ||||
|     @if (list.documents?.length > 15) { | ||||
|       <div class="mt-3"> | ||||
|         <ng-container *ngTemplateOutlet="pagination"></ng-container> | ||||
|       </div> | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -5,17 +5,25 @@ | ||||
|         <div ngbDropdown> | ||||
|           <button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button> | ||||
|           <div class="dropdown-menu shadow" ngbDropdownMenu> | ||||
|             <button *ngFor="let t of textFilterTargets" ngbDropdownItem [class.active]="textFilterTarget === t.id" (click)="changeTextFilterTarget(t.id)">{{t.name}}</button> | ||||
|             @for (t of textFilterTargets; track t) { | ||||
|               <button ngbDropdownItem [class.active]="textFilterTarget === t.id" (click)="changeTextFilterTarget(t.id)">{{t.name}}</button> | ||||
|             } | ||||
|           </div> | ||||
|         </div> | ||||
|         <select *ngIf="textFilterTarget === 'asn'" class="form-select flex-grow-0 w-auto" [(ngModel)]="textFilterModifier" (change)="textFilterModifierChange()"> | ||||
|           <option *ngFor="let m of textFilterModifiers" ngbDropdownItem [value]="m.id">{{m.label}}</option> | ||||
|         </select> | ||||
|         <button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()"> | ||||
|           <svg fill="currentColor" class="buttonicon-sm me-1"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|           </svg> | ||||
|         </button> | ||||
|         @if (textFilterTarget === 'asn') { | ||||
|           <select class="form-select flex-grow-0 w-auto" [(ngModel)]="textFilterModifier" (change)="textFilterModifierChange()"> | ||||
|             @for (m of textFilterModifiers; track m) { | ||||
|               <option ngbDropdownItem [value]="m.id">{{m.label}}</option> | ||||
|             } | ||||
|           </select> | ||||
|         } | ||||
|         @if (_textFilter) { | ||||
|           <button class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()"> | ||||
|             <svg fill="currentColor" class="buttonicon-sm me-1"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|             </svg> | ||||
|           </button> | ||||
|         } | ||||
|         <input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget === 'fulltext-morelike'"> | ||||
|       </div> | ||||
|     </div> | ||||
| @@ -31,7 +39,7 @@ | ||||
|           (selectionModelChange)="updateRules()" | ||||
|           (opened)="onTagsDropdownOpen()" | ||||
|           [documentCounts]="tagDocumentCounts" | ||||
|           [allowSelectNone]="true"></pngx-filterable-dropdown> | ||||
|         [allowSelectNone]="true"></pngx-filterable-dropdown> | ||||
|         <pngx-filterable-dropdown class="flex-fill" title="Correspondent" icon="person-fill" i18n-title | ||||
|           filterPlaceholder="Filter correspondents" i18n-filterPlaceholder | ||||
|           [items]="correspondents" | ||||
| @@ -39,7 +47,7 @@ | ||||
|           (selectionModelChange)="updateRules()" | ||||
|           (opened)="onCorrespondentDropdownOpen()" | ||||
|           [documentCounts]="correspondentDocumentCounts" | ||||
|           [allowSelectNone]="true"></pngx-filterable-dropdown> | ||||
|         [allowSelectNone]="true"></pngx-filterable-dropdown> | ||||
|         <pngx-filterable-dropdown class="flex-fill" title="Document type" icon="file-earmark-fill" i18n-title | ||||
|           filterPlaceholder="Filter document types" i18n-filterPlaceholder | ||||
|           [items]="documentTypes" | ||||
| @@ -47,7 +55,7 @@ | ||||
|           (selectionModelChange)="updateRules()" | ||||
|           (opened)="onDocumentTypeDropdownOpen()" | ||||
|           [documentCounts]="documentTypeDocumentCounts" | ||||
|           [allowSelectNone]="true"></pngx-filterable-dropdown> | ||||
|         [allowSelectNone]="true"></pngx-filterable-dropdown> | ||||
|         <pngx-filterable-dropdown class="flex-fill" title="Storage path" icon="folder-fill" i18n-title | ||||
|           filterPlaceholder="Filter storage paths" i18n-filterPlaceholder | ||||
|           [items]="storagePaths" | ||||
| @@ -55,7 +63,7 @@ | ||||
|           (selectionModelChange)="updateRules()" | ||||
|           (opened)="onStoragePathDropdownOpen()" | ||||
|           [documentCounts]="storagePathDocumentCounts" | ||||
|           [allowSelectNone]="true"></pngx-filterable-dropdown> | ||||
|         [allowSelectNone]="true"></pngx-filterable-dropdown> | ||||
|       </div> | ||||
|       <div class="d-flex flex-wrap gap-2"> | ||||
|         <pngx-date-dropdown | ||||
| @@ -63,27 +71,27 @@ | ||||
|           (datesSet)="updateRules()" | ||||
|           [(dateBefore)]="dateCreatedBefore" | ||||
|           [(dateAfter)]="dateCreatedAfter" | ||||
|           [(relativeDate)]="dateCreatedRelativeDate"></pngx-date-dropdown> | ||||
|         [(relativeDate)]="dateCreatedRelativeDate"></pngx-date-dropdown> | ||||
|         <pngx-date-dropdown | ||||
|           title="Added" i18n-title | ||||
|           (datesSet)="updateRules()" | ||||
|           [(dateBefore)]="dateAddedBefore" | ||||
|           [(dateAfter)]="dateAddedAfter" | ||||
|           [(relativeDate)]="dateAddedRelativeDate"></pngx-date-dropdown> | ||||
|         [(relativeDate)]="dateAddedRelativeDate"></pngx-date-dropdown> | ||||
|       </div> | ||||
|       <div class="d-flex flex-wrap"> | ||||
|         <pngx-permissions-filter-dropdown | ||||
|           title="Permissions" i18n-title | ||||
|           (ownerFilterSet)="updateRules()" | ||||
|           [(selectionModel)]="permissionsSelectionModel"></pngx-permissions-filter-dropdown> | ||||
|         [(selectionModel)]="permissionsSelectionModel"></pngx-permissions-filter-dropdown> | ||||
|       </div> | ||||
|       <div class="d-flex flex-wrap d-none d-sm-inline-block"> | ||||
|         <button class="btn btn-outline-secondary btn-sm" [disabled]="!rulesModified" (click)="resetSelected()"> | ||||
|           <svg class="toolbaricon ms-n1" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#x"></use> | ||||
|           </svg><ng-container i18n>Reset filters</ng-container> | ||||
|         </button> | ||||
|             </svg><ng-container i18n>Reset filters</ng-container> | ||||
|           </button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -1726,4 +1726,13 @@ describe('FilterEditorComponent', () => { | ||||
|     ) | ||||
|     expect(component.textFilter).toEqual('') | ||||
|   }) | ||||
|  | ||||
|   it('should adjust text filter targets if more like search', () => { | ||||
|     const TEXT_FILTER_TARGET_FULLTEXT_MORELIKE = 'fulltext-morelike' // private const | ||||
|     component.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_MORELIKE | ||||
|     expect(component.textFilterTargets).toContainEqual({ | ||||
|       id: TEXT_FILTER_TARGET_FULLTEXT_MORELIKE, | ||||
|       name: $localize`More like`, | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|   | ||||
| @@ -105,6 +105,51 @@ const RELATIVE_DATE_QUERYSTRINGS = [ | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| const DEFAULT_TEXT_FILTER_TARGET_OPTIONS = [ | ||||
|   { id: TEXT_FILTER_TARGET_TITLE, name: $localize`Title` }, | ||||
|   { | ||||
|     id: TEXT_FILTER_TARGET_TITLE_CONTENT, | ||||
|     name: $localize`Title & content`, | ||||
|   }, | ||||
|   { id: TEXT_FILTER_TARGET_ASN, name: $localize`ASN` }, | ||||
|   { | ||||
|     id: TEXT_FILTER_TARGET_CUSTOM_FIELDS, | ||||
|     name: $localize`Custom fields`, | ||||
|   }, | ||||
|   { | ||||
|     id: TEXT_FILTER_TARGET_FULLTEXT_QUERY, | ||||
|     name: $localize`Advanced search`, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| const TEXT_FILTER_TARGET_MORELIKE_OPTION = { | ||||
|   id: TEXT_FILTER_TARGET_FULLTEXT_MORELIKE, | ||||
|   name: $localize`More like`, | ||||
| } | ||||
|  | ||||
| const DEFAULT_TEXT_FILTER_MODIFIER_OPTIONS = [ | ||||
|   { | ||||
|     id: TEXT_FILTER_MODIFIER_EQUALS, | ||||
|     label: $localize`equals`, | ||||
|   }, | ||||
|   { | ||||
|     id: TEXT_FILTER_MODIFIER_NULL, | ||||
|     label: $localize`is empty`, | ||||
|   }, | ||||
|   { | ||||
|     id: TEXT_FILTER_MODIFIER_NOTNULL, | ||||
|     label: $localize`is not empty`, | ||||
|   }, | ||||
|   { | ||||
|     id: TEXT_FILTER_MODIFIER_GT, | ||||
|     label: $localize`greater than`, | ||||
|   }, | ||||
|   { | ||||
|     id: TEXT_FILTER_MODIFIER_LT, | ||||
|     label: $localize`less than`, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'pngx-filter-editor', | ||||
|   templateUrl: './filter-editor.component.html', | ||||
| @@ -200,29 +245,12 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|   _moreLikeDoc: PaperlessDocument | ||||
|  | ||||
|   get textFilterTargets() { | ||||
|     let targets = [ | ||||
|       { id: TEXT_FILTER_TARGET_TITLE, name: $localize`Title` }, | ||||
|       { | ||||
|         id: TEXT_FILTER_TARGET_TITLE_CONTENT, | ||||
|         name: $localize`Title & content`, | ||||
|       }, | ||||
|       { id: TEXT_FILTER_TARGET_ASN, name: $localize`ASN` }, | ||||
|       { | ||||
|         id: TEXT_FILTER_TARGET_CUSTOM_FIELDS, | ||||
|         name: $localize`Custom fields`, | ||||
|       }, | ||||
|       { | ||||
|         id: TEXT_FILTER_TARGET_FULLTEXT_QUERY, | ||||
|         name: $localize`Advanced search`, | ||||
|       }, | ||||
|     ] | ||||
|     if (this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE) { | ||||
|       targets.push({ | ||||
|         id: TEXT_FILTER_TARGET_FULLTEXT_MORELIKE, | ||||
|         name: $localize`More like`, | ||||
|       }) | ||||
|       return DEFAULT_TEXT_FILTER_TARGET_OPTIONS.concat([ | ||||
|         TEXT_FILTER_TARGET_MORELIKE_OPTION, | ||||
|       ]) | ||||
|     } | ||||
|     return targets | ||||
|     return DEFAULT_TEXT_FILTER_TARGET_OPTIONS | ||||
|   } | ||||
|  | ||||
|   textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT | ||||
| @@ -235,28 +263,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|   public textFilterModifier: string | ||||
|  | ||||
|   get textFilterModifiers() { | ||||
|     return [ | ||||
|       { | ||||
|         id: TEXT_FILTER_MODIFIER_EQUALS, | ||||
|         label: $localize`equals`, | ||||
|       }, | ||||
|       { | ||||
|         id: TEXT_FILTER_MODIFIER_NULL, | ||||
|         label: $localize`is empty`, | ||||
|       }, | ||||
|       { | ||||
|         id: TEXT_FILTER_MODIFIER_NOTNULL, | ||||
|         label: $localize`is not empty`, | ||||
|       }, | ||||
|       { | ||||
|         id: TEXT_FILTER_MODIFIER_GT, | ||||
|         label: $localize`greater than`, | ||||
|       }, | ||||
|       { | ||||
|         id: TEXT_FILTER_MODIFIER_LT, | ||||
|         label: $localize`less than`, | ||||
|       }, | ||||
|     ] | ||||
|     return DEFAULT_TEXT_FILTER_MODIFIER_OPTIONS | ||||
|   } | ||||
|  | ||||
|   get textFilterModifierIsNull(): boolean { | ||||
|   | ||||
| @@ -8,11 +8,13 @@ | ||||
|     <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> | ||||
|     <pngx-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></pngx-input-check> | ||||
|     <pngx-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></pngx-input-check> | ||||
|     <div *ngIf="error?.filter_rules" class="alert alert-danger" role="alert"> | ||||
|       <h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6> | ||||
|       <ng-container i18n>The error returned was</ng-container>:<br/> | ||||
|       {{ error.filter_rules }} | ||||
|     </div> | ||||
|     @if (error?.filter_rules) { | ||||
|       <div class="alert alert-danger" role="alert"> | ||||
|         <h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6> | ||||
|         <ng-container i18n>The error returned was</ng-container>:<br/> | ||||
|         {{ error.filter_rules }} | ||||
|       </div> | ||||
|     } | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="!buttonsEnabled">Cancel</button> | ||||
|   | ||||
| @@ -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> | ||||
|         <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> | ||||
|             <div class="invalid-feedback" i18n> | ||||
|       <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> | ||||
|         <div class="invalid-feedback" i18n> | ||||
|                 Please enter a note. | ||||
|             </div> | ||||
|         </div> | ||||
|         <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> | ||||
|             <button type="button" class="btn btn-primary btn-sm" [disabled]="networkActive || addDisabled" (click)="addNote()" i18n>Add note</button> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="form-group mt-2 d-flex justify-content-end align-items-center"> | ||||
|         @if (networkActive) { | ||||
|           <div class="spinner-border spinner-border-sm fw-normal me-auto" role="status"></div> | ||||
|         } | ||||
|         <button type="button" class="btn btn-primary btn-sm" [disabled]="networkActive || addDisabled" (click)="addNote()" i18n>Add note</button> | ||||
|       </div> | ||||
|     </form> | ||||
|     <hr> | ||||
|     <div *ngFor="let note of notes" class="card border mb-3"> | ||||
|         <div class="card-body text-dark"> | ||||
|       @for (note of notes; track note) { | ||||
|         <div class="card border mb-3"> | ||||
|           <div class="card-body text-dark"> | ||||
|             <p class="card-text">{{note.note}}</p> | ||||
|         </div> | ||||
|         <div class="d-flex card-footer small bg-light text-primary justify-content-between align-items-center"> | ||||
|           </div> | ||||
|           <div class="d-flex card-footer small bg-light text-primary justify-content-between align-items-center"> | ||||
|             <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 }"> | ||||
|                 <svg width="13" height="13" fill="currentColor"> | ||||
|                     <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                 </svg> | ||||
|                 <span class="visually-hidden" i18n>Delete note</span> | ||||
|               <svg width="13" height="13" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|               </svg> | ||||
|               <span class="visually-hidden" i18n>Delete note</span> | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|       } | ||||
|     </div> | ||||
| </div> | ||||
|   } | ||||
|   | ||||
| @@ -1,43 +1,47 @@ | ||||
| <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 }"> | ||||
|       <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|       </svg> | ||||
|       <ng-container i18n>Add Template</ng-container> | ||||
|     </button> | ||||
|   <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"> | ||||
|       <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|     </svg> | ||||
|     <ng-container i18n>Add Template</ng-container> | ||||
|   </button> | ||||
| </pngx-page-header> | ||||
|  | ||||
| <ul class="list-group"> | ||||
|  | ||||
|     <li class="list-group-item"> | ||||
|         <div class="row"> | ||||
|             <div class="col" i18n>Name</div> | ||||
|             <div class="col" i18n>Sort order</div> | ||||
|             <div class="col" i18n>Document Sources</div> | ||||
|             <div class="col" i18n>Actions</div> | ||||
|         </div> | ||||
|     </li> | ||||
|   <li class="list-group-item"> | ||||
|     <div class="row"> | ||||
|       <div class="col" i18n>Name</div> | ||||
|       <div class="col" i18n>Sort order</div> | ||||
|       <div class="col" i18n>Document Sources</div> | ||||
|       <div class="col" i18n>Actions</div> | ||||
|     </div> | ||||
|   </li> | ||||
|  | ||||
|     <li *ngFor="let template of templates" 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)="editTemplate(template)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.ConsumptionTemplate)">{{template.name}}</button></div> | ||||
|             <div class="col d-flex align-items-center"><code>{{template.order}}</code></div> | ||||
|             <div class="col d-flex align-items-center">{{getSourceList(template)}}</div> | ||||
|             <div class="col"> | ||||
|                 <div class="btn-group"> | ||||
|                     <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editTemplate(template)"> | ||||
|                     <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#pencil" /> | ||||
|                       </svg> <ng-container i18n>Edit</ng-container> | ||||
|                     </button> | ||||
|                     <button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteTemplate(template)"> | ||||
|                         <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                             <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                         </svg> <ng-container i18n>Delete</ng-container> | ||||
|                     </button> | ||||
|                 </div> | ||||
|   @for (template of templates; track template) { | ||||
|     <li 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)="editTemplate(template)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.ConsumptionTemplate)">{{template.name}}</button></div> | ||||
|         <div class="col d-flex align-items-center"><code>{{template.order}}</code></div> | ||||
|         <div class="col d-flex align-items-center">{{getSourceList(template)}}</div> | ||||
|         <div class="col"> | ||||
|           <div class="btn-group"> | ||||
|             <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editTemplate(template)"> | ||||
|               <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#pencil" /> | ||||
|                 </svg> <ng-container i18n>Edit</ng-container> | ||||
|               </button> | ||||
|               <button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteTemplate(template)"> | ||||
|                 <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                   </svg> <ng-container i18n>Delete</ng-container> | ||||
|                 </button> | ||||
|               </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </li> | ||||
|     <li *ngIf="templates.length === 0" class="list-group-item" i18n>No templates defined.</li> | ||||
| </ul> | ||||
|           </div> | ||||
|         </li> | ||||
|       } | ||||
|       @if (templates.length === 0) { | ||||
|         <li class="list-group-item" i18n>No templates defined.</li> | ||||
|       } | ||||
|     </ul> | ||||
|   | ||||
| @@ -1,41 +1,45 @@ | ||||
| <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 }"> | ||||
|       <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|       </svg> | ||||
|       <ng-container i18n>Add Field</ng-container> | ||||
|     </button> | ||||
|   <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"> | ||||
|       <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|     </svg> | ||||
|     <ng-container i18n>Add Field</ng-container> | ||||
|   </button> | ||||
| </pngx-page-header> | ||||
|  | ||||
| <ul class="list-group"> | ||||
|  | ||||
|     <li class="list-group-item"> | ||||
|         <div class="row"> | ||||
|             <div class="col" i18n>Name</div> | ||||
|             <div class="col" i18n>Data Type</div> | ||||
|             <div class="col" i18n>Actions</div> | ||||
|         </div> | ||||
|     </li> | ||||
|   <li class="list-group-item"> | ||||
|     <div class="row"> | ||||
|       <div class="col" i18n>Name</div> | ||||
|       <div class="col" i18n>Data Type</div> | ||||
|       <div class="col" i18n>Actions</div> | ||||
|     </div> | ||||
|   </li> | ||||
|  | ||||
|     <li *ngFor="let field of fields" 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)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div> | ||||
|             <div class="col d-flex align-items-center">{{getDataType(field)}}</div> | ||||
|             <div class="col"> | ||||
|                 <div class="btn-group"> | ||||
|                     <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editField(field)"> | ||||
|                     <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#pencil" /> | ||||
|                       </svg> <ng-container i18n>Edit</ng-container> | ||||
|                     </button> | ||||
|                     <button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteField(field)"> | ||||
|                         <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                             <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                         </svg> <ng-container i18n>Delete</ng-container> | ||||
|                     </button> | ||||
|                 </div> | ||||
|   @for (field of fields; track field) { | ||||
|     <li 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)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div> | ||||
|         <div class="col d-flex align-items-center">{{getDataType(field)}}</div> | ||||
|         <div class="col"> | ||||
|           <div class="btn-group"> | ||||
|             <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editField(field)"> | ||||
|               <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#pencil" /> | ||||
|                 </svg> <ng-container i18n>Edit</ng-container> | ||||
|               </button> | ||||
|               <button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteField(field)"> | ||||
|                 <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                   </svg> <ng-container i18n>Delete</ng-container> | ||||
|                 </button> | ||||
|               </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </li> | ||||
|     <li *ngIf="fields.length === 0" class="list-group-item" i18n>No fields defined.</li> | ||||
| </ul> | ||||
|           </div> | ||||
|         </li> | ||||
|       } | ||||
|       @if (fields.length === 0) { | ||||
|         <li class="list-group-item" i18n>No fields defined.</li> | ||||
|       } | ||||
|     </ul> | ||||
|   | ||||
| @@ -2,104 +2,114 @@ | ||||
| </pngx-page-header> | ||||
|  | ||||
| <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"> | ||||
|     <h4> | ||||
|         <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 }"> | ||||
|         <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|         </svg> | ||||
|         <ng-container i18n>Add Account</ng-container> | ||||
|         </button> | ||||
|     </h4> | ||||
|     <ul class="list-group"> | ||||
|         <li class="list-group-item"> | ||||
|             <div class="row"> | ||||
|             <div class="col" i18n>Name</div> | ||||
|             <div class="col" i18n>Server</div> | ||||
|             <div class="col" i18n>Actions</div> | ||||
|             </div> | ||||
|         </li> | ||||
|   <h4> | ||||
|     <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 }"> | ||||
|       <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|       </svg> | ||||
|       <ng-container i18n>Add Account</ng-container> | ||||
|     </button> | ||||
|   </h4> | ||||
|   <ul class="list-group"> | ||||
|     <li class="list-group-item"> | ||||
|       <div class="row"> | ||||
|         <div class="col" i18n>Name</div> | ||||
|         <div class="col" i18n>Server</div> | ||||
|         <div class="col" i18n>Actions</div> | ||||
|       </div> | ||||
|     </li> | ||||
|  | ||||
|         <li *ngFor="let account of mailAccounts" 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)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">{{account.name}}</button></div> | ||||
|             <div class="col d-flex align-items-center">{{account.imap_server}}</div> | ||||
|             <div class="col"> | ||||
|                 <div class="btn-group"> | ||||
|                 <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" [disabled]="!userCanEdit(account)" class="btn btn-sm btn-outline-secondary" type="button" (click)="editMailAccount(account)"> | ||||
|                     <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#pencil" /> | ||||
|                     </svg> <ng-container i18n>Edit</ng-container> | ||||
|     @for (account of mailAccounts; track account) { | ||||
|       <li 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)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">{{account.name}}</button></div> | ||||
|           <div class="col d-flex align-items-center">{{account.imap_server}}</div> | ||||
|           <div class="col"> | ||||
|             <div class="btn-group"> | ||||
|               <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" [disabled]="!userCanEdit(account)" class="btn btn-sm btn-outline-secondary" type="button" (click)="editMailAccount(account)"> | ||||
|                 <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#pencil" /> | ||||
|                   </svg> <ng-container i18n>Edit</ng-container> | ||||
|                 </button> | ||||
|                 <button *pngxIfOwner="account" class="btn btn-sm btn-outline-secondary" type="button" (click)="editPermissions(account)"> | ||||
|                     <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#person-lock" /> | ||||
|                   <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                     <use xlink:href="assets/bootstrap-icons.svg#person-lock" /> | ||||
|                     </svg> <ng-container i18n>Permissions</ng-container> | ||||
|                 </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> | ||||
|                   <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"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                     </svg> <ng-container i18n>Delete</ng-container> | ||||
|                 </button> | ||||
|                       <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                       </svg> <ng-container i18n>Delete</ng-container> | ||||
|                     </button> | ||||
|                   </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             </div> | ||||
|         </li> | ||||
|         <li *ngIf="mailAccounts.length === 0" class="list-group-item" i18n>No mail accounts defined.</li> | ||||
|     </ul> | ||||
|               </div> | ||||
|             </li> | ||||
|           } | ||||
|           @if (mailAccounts.length === 0) { | ||||
|             <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 }"> | ||||
|     <h4 class="mt-4"> | ||||
|         <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 }"> | ||||
|         <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|         </svg> | ||||
|         <ng-container i18n>Add Rule</ng-container> | ||||
|         </button> | ||||
|     </h4> | ||||
|     <ul class="list-group"> | ||||
|         <li class="list-group-item"> | ||||
|       <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }"> | ||||
|         <h4 class="mt-4"> | ||||
|           <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 }"> | ||||
|             <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|               <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|             </svg> | ||||
|             <ng-container i18n>Add Rule</ng-container> | ||||
|           </button> | ||||
|         </h4> | ||||
|         <ul class="list-group"> | ||||
|           <li class="list-group-item"> | ||||
|             <div class="row"> | ||||
|             <div class="col" i18n>Name</div> | ||||
|             <div class="col" i18n>Account</div> | ||||
|             <div class="col" i18n>Actions</div> | ||||
|               <div class="col" i18n>Name</div> | ||||
|               <div class="col" i18n>Account</div> | ||||
|               <div class="col" i18n>Actions</div> | ||||
|             </div> | ||||
|         </li> | ||||
|           </li> | ||||
|  | ||||
|         <li *ngFor="let rule of mailRules" 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)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div> | ||||
|             <div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div> | ||||
|             <div class="col"> | ||||
|                 <div class="btn-group"> | ||||
|                 <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"> | ||||
|           @for (rule of mailRules; track rule) { | ||||
|             <li 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)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div> | ||||
|                 <div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div> | ||||
|                 <div class="col"> | ||||
|                   <div class="btn-group"> | ||||
|                     <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" /> | ||||
|                     </svg> <ng-container i18n>Edit</ng-container> | ||||
|                 </button> | ||||
|                 <button *pngxIfOwner="rule" class="btn btn-sm btn-outline-secondary" type="button" (click)="editPermissions(rule)"> | ||||
|                     <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#person-lock" /> | ||||
|                     </svg> <ng-container i18n>Permissions</ng-container> | ||||
|                 </button> | ||||
|                 <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"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                     </svg> <ng-container i18n>Delete</ng-container> | ||||
|                 </button> | ||||
|                 </div> | ||||
|             </div> | ||||
|             </div> | ||||
|         </li> | ||||
|         <li *ngIf="mailRules.length === 0" class="list-group-item" i18n>No mail rules defined.</li> | ||||
|     </ul> | ||||
|                         </svg> <ng-container i18n>Edit</ng-container> | ||||
|                       </button> | ||||
|                       <button *pngxIfOwner="rule" class="btn btn-sm btn-outline-secondary" type="button" (click)="editPermissions(rule)"> | ||||
|                         <svg class="buttonicon-sm" fill="currentColor"> | ||||
|                           <use xlink:href="assets/bootstrap-icons.svg#person-lock" /> | ||||
|                           </svg> <ng-container i18n>Permissions</ng-container> | ||||
|                         </button> | ||||
|                         <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"> | ||||
|                             <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|                             </svg> <ng-container i18n>Delete</ng-container> | ||||
|                           </button> | ||||
|                         </div> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   </li> | ||||
|                 } | ||||
|                 @if (mailRules.length === 0) { | ||||
|                   <li class="list-group-item" i18n>No mail rules defined.</li> | ||||
|                 } | ||||
|               </ul> | ||||
|  | ||||
| </ng-container> | ||||
|             </ng-container> | ||||
|  | ||||
| <div *ngIf="!mailAccounts || !mailRules"> | ||||
|     <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||
|     <div class="visually-hidden" i18n>Loading...</div> | ||||
| </div> | ||||
|             @if (!mailAccounts || !mailRules) { | ||||
|               <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> | ||||
|             } | ||||
|   | ||||
| @@ -2,112 +2,131 @@ | ||||
|   <button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedObjects.size === 0"> | ||||
|     <svg class="sidebaricon" fill="currentColor"> | ||||
|       <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|     </svg> <ng-container i18n>Clear selection</ng-container> | ||||
|   </button> | ||||
|   <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"> | ||||
|       <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" /> | ||||
|     </svg> <ng-container i18n>Permissions</ng-container> | ||||
|   </button> | ||||
|   <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }" i18n> | ||||
|       </svg> <ng-container i18n>Clear selection</ng-container> | ||||
|     </button> | ||||
|     <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"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" /> | ||||
|         </svg> <ng-container i18n>Permissions</ng-container> | ||||
|       </button> | ||||
|       <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"> | ||||
|       <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|     </svg> | ||||
|     Create | ||||
|   </button> | ||||
| </pngx-page-header> | ||||
|     </pngx-page-header> | ||||
|  | ||||
| <div class="row mb-3"> | ||||
|   <div class="col-md mb-2 mb-xl-0"> | ||||
|     <div class="form-inline d-flex align-items-center"> | ||||
|       <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> | ||||
|     <div class="row mb-3"> | ||||
|       <div class="col-md mb-2 mb-xl-0"> | ||||
|         <div class="form-inline d-flex align-items-center"> | ||||
|           <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> | ||||
|         </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> | ||||
|  | ||||
|   <ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination> | ||||
| </div> | ||||
|  | ||||
| <div class="card border mb-3"> | ||||
| <table class="table table-striped align-middle shadow-sm mb-0"> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th scope="col"> | ||||
|         <div class="form-check m-0 ms-2 me-n2"> | ||||
|           <input type="checkbox" class="form-check-input" id="all-objects" [disabled]="data.length === 0" (click)="toggleAll($event); $event.stopPropagation();"> | ||||
|           <label class="form-check-label" for="all-objects"></label> | ||||
|         </div> | ||||
|       </th> | ||||
|       <th scope="col" class="fw-normal" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th> | ||||
|       <th scope="col" class="fw-normal d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th> | ||||
|       <th scope="col" class="fw-normal" 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> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr *ngIf="isLoading"> | ||||
|       <td colspan="5"> | ||||
|         <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|         <ng-container i18n>Loading...</ng-container> | ||||
|       </td> | ||||
|     </tr> | ||||
|     <tr *ngFor="let object of data" (click)="toggleSelected(object, $event); $event.stopPropagation();"> | ||||
|       <td> | ||||
|         <div class="form-check m-0 ms-2 me-n2"> | ||||
|           <input type="checkbox" class="form-check-input" id="{{typeName}}{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();"> | ||||
|           <label class="form-check-label" for="{{typeName}}{{object.id}}"></label> | ||||
|         </div> | ||||
|       </td> | ||||
|       <td scope="row"><button class="btn btn-link ms-0 ps-0 text-start" (click)="openEditDialog(object)">{{ object.name }}</button> </td> | ||||
|       <td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td> | ||||
|       <td scope="row">{{ object.document_count }}</td> | ||||
|       <td scope="row" *ngFor="let column of extraColumns"> | ||||
|         <div *ngIf="column.rendersHtml; else colValue" [innerHtml]="column.valueFn.call(null, object) | safeHtml"></div> | ||||
|         <ng-template #colValue>{{ column.valueFn.call(null, object) }}</ng-template> | ||||
|       </td> | ||||
|       <td scope="row"> | ||||
|         <div class="btn-group d-block d-sm-none"> | ||||
|           <div ngbDropdown class="d-inline-block"> | ||||
|             <button type="button" class="btn btn-link" id="actionsMenuMobile" ngbDropdownToggle> | ||||
|               <svg class="toolbaricon" fill="currentColor"> | ||||
|                 <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 class="card border mb-3"> | ||||
|       <table class="table table-striped align-middle shadow-sm mb-0"> | ||||
|         <thead> | ||||
|           <tr> | ||||
|             <th scope="col"> | ||||
|               <div class="form-check m-0 ms-2 me-n2"> | ||||
|                 <input type="checkbox" class="form-check-input" id="all-objects" [disabled]="data.length === 0" (click)="toggleAll($event); $event.stopPropagation();"> | ||||
|                 <label class="form-check-label" for="all-objects"></label> | ||||
|               </div> | ||||
|             </th> | ||||
|             <th scope="col" class="fw-normal" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th> | ||||
|             <th scope="col" class="fw-normal d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th> | ||||
|             <th scope="col" class="fw-normal" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th> | ||||
|             @for (column of extraColumns; track column) { | ||||
|               <th scope="col" class="fw-normal" pngxSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th> | ||||
|             } | ||||
|             <th scope="col" class="fw-normal" i18n>Actions</th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           @if (isLoading) { | ||||
|             <tr> | ||||
|               <td colspan="5"> | ||||
|                 <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|                 <ng-container i18n>Loading...</ng-container> | ||||
|               </td> | ||||
|             </tr> | ||||
|           } | ||||
|           @for (object of data; track object) { | ||||
|             <tr (click)="toggleSelected(object); $event.stopPropagation();"> | ||||
|               <td> | ||||
|                 <div class="form-check m-0 ms-2 me-n2"> | ||||
|                   <input type="checkbox" class="form-check-input" id="{{typeName}}{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();"> | ||||
|                   <label class="form-check-label" for="{{typeName}}{{object.id}}"></label> | ||||
|                 </div> | ||||
|               </td> | ||||
|               <td scope="row"><button class="btn btn-link ms-0 ps-0 text-start" (click)="openEditDialog(object)">{{ object.name }}</button> </td> | ||||
|               <td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td> | ||||
|               <td scope="row">{{ object.document_count }}</td> | ||||
|               @for (column of extraColumns; track column) { | ||||
|                 <td scope="row"> | ||||
|                   @if (column.rendersHtml) { | ||||
|                     <div [innerHtml]="column.valueFn.call(null, object) | safeHtml"></div> | ||||
|                   } @else { | ||||
|                     {{ column.valueFn.call(null, object) }} | ||||
|                   } | ||||
|                 </td> | ||||
|               } | ||||
|               <td scope="row"> | ||||
|                 <div class="btn-group d-block d-sm-none"> | ||||
|                   <div ngbDropdown class="d-inline-block"> | ||||
|                     <button type="button" class="btn btn-link" id="actionsMenuMobile" ngbDropdownToggle> | ||||
|                       <svg class="toolbaricon" fill="currentColor"> | ||||
|                         <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> <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> <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> <ng-container i18n>Delete</ng-container> | ||||
|                         </button> | ||||
|                       </div> | ||||
|                     </td> | ||||
|                   </tr> | ||||
|                 } | ||||
|               </tbody> | ||||
|             </table> | ||||
|           </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> <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> <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> <ng-container i18n>Delete</ng-container> | ||||
|           </button> | ||||
|         </div> | ||||
|       </td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
| </div> | ||||
|  | ||||
| <div class="d-flex mb-2" *ngIf="!isLoading"> | ||||
|   <div *ngIf="collectionSize > 0"> | ||||
|     <ng-container i18n>{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</ng-container> | ||||
|     <ng-container *ngIf="selectedObjects.size > 0"> ({{selectedObjects.size}} selected)</ng-container> | ||||
|   </div> | ||||
|   <ngb-pagination *ngIf="collectionSize > 20" class="ms-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination> | ||||
| </div> | ||||
|           @if (!isLoading) { | ||||
|             <div class="d-flex mb-2"> | ||||
|               @if (collectionSize > 0) { | ||||
|                 <div> | ||||
|                   <ng-container i18n>{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</ng-container> | ||||
|                   @if (selectedObjects.size > 0) { | ||||
|                      ({{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> | ||||
|           } | ||||
|   | ||||
| @@ -38,6 +38,201 @@ export interface LanguageOption { | ||||
|   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({ | ||||
|   providedIn: 'root', | ||||
| }) | ||||
| @@ -151,210 +346,14 @@ export class SettingsService { | ||||
|   } | ||||
|  | ||||
|   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 | ||||
|     languages.sort((a, b) => { | ||||
|     return LANGUAGE_OPTIONS.sort((a, b) => { | ||||
|       return a.name < b.name ? -1 : 1 | ||||
|     }) | ||||
|  | ||||
|     return languages | ||||
|   } | ||||
|  | ||||
|   getDateLocaleOptions(): LanguageOption[] { | ||||
|     let isoOption: LanguageOption = { | ||||
|       code: 'iso-8601', | ||||
|       name: $localize`ISO 8601`, | ||||
|       dateInputFormat: 'yyyy-mm-dd', | ||||
|     } | ||||
|     return [isoOption].concat(this.getLanguageOptions()) | ||||
|     return [ISO_LANGUAGE_OPTION].concat(this.getLanguageOptions()) | ||||
|   } | ||||
|  | ||||
|   private getLanguageCookieName() { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon