mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-09-16 21:55:37 -05:00
Merge branch 'dev' into feature-nested-tags2
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
<div class="selected-icon">
|
||||
@if (createdRelativeDate) {
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearCreatedRelativeDate()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused text-dark"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
|
@@ -28,6 +28,16 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (allSelectOptions.length > SELECT_OPTION_PAGE_SIZE) {
|
||||
<ngb-pagination
|
||||
class="d-flex justify-content-end"
|
||||
[pageSize]="SELECT_OPTION_PAGE_SIZE"
|
||||
[collectionSize]="allSelectOptions.length"
|
||||
[(page)]="selectOptionsPage"
|
||||
[maxSize]="5"
|
||||
size="sm"
|
||||
></ngb-pagination>
|
||||
}
|
||||
@if (object?.id) {
|
||||
<small class="d-block mt-2" i18n>Warning: existing instances of this field will retain their current value index (e.g. option #1, #2, #3) after editing the options here</small>
|
||||
}
|
||||
|
@@ -125,4 +125,42 @@ describe('CustomFieldEditDialogComponent', () => {
|
||||
fixture.detectChanges()
|
||||
expect(document.activeElement).toBe(selectOptionInputs.last.nativeElement)
|
||||
})
|
||||
|
||||
it('should send all select options including those changed in form on save', () => {
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
component.object = {
|
||||
id: 1,
|
||||
name: 'Field 1',
|
||||
data_type: CustomFieldDataType.Select,
|
||||
extra_data: {
|
||||
select_options: Array.from({ length: 50 }, (_, i) => ({
|
||||
label: `Option ${i + 1}`,
|
||||
id: `${i + 1}-xyz`,
|
||||
})),
|
||||
},
|
||||
}
|
||||
fixture.detectChanges()
|
||||
component.ngOnInit()
|
||||
component.selectOptionsPage = 2
|
||||
fixture.detectChanges()
|
||||
component.objectForm
|
||||
.get('extra_data')
|
||||
.get('select_options')
|
||||
.get('0')
|
||||
.get('label')
|
||||
.setValue('Updated Option 9')
|
||||
const formValues = (component as any).getFormValues()
|
||||
// first item unchanged
|
||||
expect(formValues.extra_data.select_options[0]).toEqual({
|
||||
label: 'Option 1',
|
||||
id: '1-xyz',
|
||||
})
|
||||
// page 2 first item updated
|
||||
expect(
|
||||
formValues.extra_data.select_options[component.SELECT_OPTION_PAGE_SIZE]
|
||||
).toEqual({
|
||||
label: 'Updated Option 9',
|
||||
id: '9-xyz',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@@ -14,6 +14,7 @@ import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { takeUntil } from 'rxjs'
|
||||
import {
|
||||
@@ -28,6 +29,8 @@ import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
|
||||
|
||||
const SELECT_OPTION_PAGE_SIZE = 8
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-custom-field-edit-dialog',
|
||||
templateUrl: './custom-field-edit-dialog.component.html',
|
||||
@@ -37,6 +40,7 @@ import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
|
||||
TextComponent,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbPaginationModule,
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
})
|
||||
@@ -45,6 +49,21 @@ export class CustomFieldEditDialogComponent
|
||||
implements OnInit, AfterViewInit
|
||||
{
|
||||
CustomFieldDataType = CustomFieldDataType
|
||||
SELECT_OPTION_PAGE_SIZE = SELECT_OPTION_PAGE_SIZE
|
||||
|
||||
private _allSelectOptions: any[] = []
|
||||
public get allSelectOptions(): any[] {
|
||||
return this._allSelectOptions
|
||||
}
|
||||
|
||||
private _selectOptionsPage: number
|
||||
public get selectOptionsPage(): number {
|
||||
return this._selectOptionsPage
|
||||
}
|
||||
public set selectOptionsPage(v: number) {
|
||||
this._selectOptionsPage = v
|
||||
this.updateSelectOptions()
|
||||
}
|
||||
|
||||
@ViewChildren('selectOption')
|
||||
private selectOptionInputs: QueryList<ElementRef>
|
||||
@@ -67,17 +86,10 @@ export class CustomFieldEditDialogComponent
|
||||
this.objectForm.get('data_type').disable()
|
||||
}
|
||||
if (this.object?.data_type === CustomFieldDataType.Select) {
|
||||
this.selectOptions.clear()
|
||||
this.object.extra_data.select_options
|
||||
.filter((option) => option)
|
||||
.forEach((option) =>
|
||||
this.selectOptions.push(
|
||||
new FormGroup({
|
||||
label: new FormControl(option.label),
|
||||
id: new FormControl(option.id),
|
||||
})
|
||||
)
|
||||
)
|
||||
this._allSelectOptions = [
|
||||
...(this.object.extra_data.select_options ?? []),
|
||||
]
|
||||
this.selectOptionsPage = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +99,19 @@ export class CustomFieldEditDialogComponent
|
||||
.subscribe(() => {
|
||||
this.selectOptionInputs.last?.nativeElement.focus()
|
||||
})
|
||||
|
||||
this.objectForm.valueChanges
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((change) => {
|
||||
// Update the relevant select options values if changed in the form, which is only a page of the entire list
|
||||
this.objectForm
|
||||
.get('extra_data.select_options')
|
||||
?.value.forEach((option, index) => {
|
||||
this._allSelectOptions[
|
||||
index + (this.selectOptionsPage - 1) * SELECT_OPTION_PAGE_SIZE
|
||||
] = option
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
@@ -108,6 +133,17 @@ export class CustomFieldEditDialogComponent
|
||||
})
|
||||
}
|
||||
|
||||
protected getFormValues() {
|
||||
const formValues = super.getFormValues()
|
||||
if (
|
||||
this.objectForm.get('data_type')?.value === CustomFieldDataType.Select
|
||||
) {
|
||||
// Make sure we send all select options, with updated values
|
||||
formValues.extra_data.select_options = this._allSelectOptions
|
||||
}
|
||||
return formValues
|
||||
}
|
||||
|
||||
getDataTypes() {
|
||||
return DATA_TYPE_LABELS
|
||||
}
|
||||
@@ -116,13 +152,35 @@ export class CustomFieldEditDialogComponent
|
||||
return this.dialogMode === EditDialogMode.EDIT
|
||||
}
|
||||
|
||||
private updateSelectOptions() {
|
||||
this.selectOptions.clear()
|
||||
this._allSelectOptions
|
||||
.slice(
|
||||
(this.selectOptionsPage - 1) * SELECT_OPTION_PAGE_SIZE,
|
||||
this.selectOptionsPage * SELECT_OPTION_PAGE_SIZE
|
||||
)
|
||||
.forEach((option) =>
|
||||
this.selectOptions.push(
|
||||
new FormGroup({
|
||||
label: new FormControl(option.label),
|
||||
id: new FormControl(option.id),
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public addSelectOption() {
|
||||
this.selectOptions.push(
|
||||
new FormGroup({ label: new FormControl(null), id: new FormControl(null) })
|
||||
this._allSelectOptions.push({ label: null, id: null })
|
||||
this.selectOptionsPage = Math.ceil(
|
||||
this.allSelectOptions.length / SELECT_OPTION_PAGE_SIZE
|
||||
)
|
||||
}
|
||||
|
||||
public removeSelectOption(index: number) {
|
||||
this.selectOptions.removeAt(index)
|
||||
this._allSelectOptions.splice(
|
||||
index + (this.selectOptionsPage - 1) * SELECT_OPTION_PAGE_SIZE,
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -147,9 +147,13 @@ export abstract class EditDialogComponent<
|
||||
)
|
||||
}
|
||||
|
||||
protected getFormValues(): any {
|
||||
return Object.assign({}, this.objectForm.value)
|
||||
}
|
||||
|
||||
save() {
|
||||
this.error = null
|
||||
const formValues = Object.assign({}, this.objectForm.value)
|
||||
const formValues = this.getFormValues()
|
||||
const permissionsObject: PermissionsFormObject =
|
||||
this.objectForm.get('permissions_form')?.value
|
||||
if (permissionsObject) {
|
||||
|
@@ -254,6 +254,18 @@
|
||||
<h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.sanity_check_error}}</span>
|
||||
}
|
||||
</ng-template>
|
||||
<dt i18n>WebSocket Connection</dt>
|
||||
<dd>
|
||||
<span class="btn btn-sm pe-none align-items-center btn-dark text-uppercase small">
|
||||
@if (status.websocket_connected === 'OK') {
|
||||
<ng-container i18n>OK</ng-container>
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||
} @else {
|
||||
<ng-container i18n>Error</ng-container>
|
||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
|
||||
}
|
||||
</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -24,7 +24,7 @@ import {
|
||||
} from '@angular/core/testing'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { Subject, of, throwError } from 'rxjs'
|
||||
import { PaperlessTaskName } from 'src/app/data/paperless-task'
|
||||
import {
|
||||
InstallType,
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||
import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
|
||||
import { SystemStatusDialogComponent } from './system-status-dialog.component'
|
||||
|
||||
const status: SystemStatus = {
|
||||
@@ -77,6 +78,8 @@ describe('SystemStatusDialogComponent', () => {
|
||||
let tasksService: TasksService
|
||||
let systemStatusService: SystemStatusService
|
||||
let toastService: ToastService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
let websocketSubject: Subject<boolean> = new Subject<boolean>()
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
@@ -98,6 +101,12 @@ describe('SystemStatusDialogComponent', () => {
|
||||
tasksService = TestBed.inject(TasksService)
|
||||
systemStatusService = TestBed.inject(SystemStatusService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
jest
|
||||
.spyOn(websocketStatusService, 'onConnectionStatus')
|
||||
.mockImplementation(() => {
|
||||
return websocketSubject.asObservable()
|
||||
})
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
@@ -168,4 +177,19 @@ describe('SystemStatusDialogComponent', () => {
|
||||
component.ngOnInit()
|
||||
expect(component.versionMismatch).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should update websocket connection status', () => {
|
||||
websocketSubject.next(true)
|
||||
expect(component.status.websocket_connected).toEqual(
|
||||
SystemStatusItemStatus.OK
|
||||
)
|
||||
websocketSubject.next(false)
|
||||
expect(component.status.websocket_connected).toEqual(
|
||||
SystemStatusItemStatus.ERROR
|
||||
)
|
||||
websocketSubject.next(true)
|
||||
expect(component.status.websocket_connected).toEqual(
|
||||
SystemStatusItemStatus.OK
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import {
|
||||
NgbActiveModal,
|
||||
NgbModalModule,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
NgbProgressbarModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { PaperlessTaskName } from 'src/app/data/paperless-task'
|
||||
import {
|
||||
SystemStatus,
|
||||
@@ -18,6 +19,7 @@ import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||
import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
@Component({
|
||||
@@ -34,13 +36,14 @@ import { environment } from 'src/environments/environment'
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
})
|
||||
export class SystemStatusDialogComponent implements OnInit {
|
||||
export class SystemStatusDialogComponent implements OnInit, OnDestroy {
|
||||
activeModal = inject(NgbActiveModal)
|
||||
private clipboard = inject(Clipboard)
|
||||
private systemStatusService = inject(SystemStatusService)
|
||||
private tasksService = inject(TasksService)
|
||||
private toastService = inject(ToastService)
|
||||
private permissionsService = inject(PermissionsService)
|
||||
private websocketStatusService = inject(WebsocketStatusService)
|
||||
|
||||
public SystemStatusItemStatus = SystemStatusItemStatus
|
||||
public PaperlessTaskName = PaperlessTaskName
|
||||
@@ -51,6 +54,7 @@ export class SystemStatusDialogComponent implements OnInit {
|
||||
public copied: boolean = false
|
||||
|
||||
private runningTasks: Set<PaperlessTaskName> = new Set()
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
get currentUserIsSuperUser(): boolean {
|
||||
return this.permissionsService.isSuperUser()
|
||||
@@ -65,6 +69,17 @@ export class SystemStatusDialogComponent implements OnInit {
|
||||
if (this.versionMismatch) {
|
||||
this.status.pngx_version = `${this.status.pngx_version} (frontend: ${this.frontendVersion})`
|
||||
}
|
||||
this.status.websocket_connected = this.websocketStatusService.isConnected()
|
||||
? SystemStatusItemStatus.OK
|
||||
: SystemStatusItemStatus.ERROR
|
||||
this.websocketStatusService
|
||||
.onConnectionStatus()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((connected) => {
|
||||
this.status.websocket_connected = connected
|
||||
? SystemStatusItemStatus.OK
|
||||
: SystemStatusItemStatus.ERROR
|
||||
})
|
||||
}
|
||||
|
||||
public close() {
|
||||
@@ -97,7 +112,7 @@ export class SystemStatusDialogComponent implements OnInit {
|
||||
this.runningTasks.delete(taskName)
|
||||
this.systemStatusService.get().subscribe({
|
||||
next: (status) => {
|
||||
this.status = status
|
||||
Object.assign(this.status, status)
|
||||
},
|
||||
})
|
||||
},
|
||||
@@ -110,4 +125,9 @@ export class SystemStatusDialogComponent implements OnInit {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(this)
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user