mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-19 10:19:27 -05:00
Basic backend migration, frontend UI. Mostly works
[ci skip]
This commit is contained in:
parent
89e5c08a1f
commit
0e76b86066
@ -188,7 +188,7 @@
|
|||||||
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
|
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
|
||||||
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></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 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>
|
<pngx-input-custom-fields-select i18n-title title="Assign custom fields" formControlName="assign_custom_fields_w_values"></pngx-input-custom-fields-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
|
<pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
|
||||||
|
@ -16,7 +16,7 @@ import { NgbAccordionModule, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
|||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { first } from 'rxjs'
|
import { first } from 'rxjs'
|
||||||
import { Correspondent } from 'src/app/data/correspondent'
|
import { Correspondent } from 'src/app/data/correspondent'
|
||||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
import { CustomField } from 'src/app/data/custom-field'
|
||||||
import { DocumentType } from 'src/app/data/document-type'
|
import { DocumentType } from 'src/app/data/document-type'
|
||||||
import { MailRule } from 'src/app/data/mail-rule'
|
import { MailRule } from 'src/app/data/mail-rule'
|
||||||
import {
|
import {
|
||||||
@ -38,7 +38,6 @@ import {
|
|||||||
WorkflowTriggerType,
|
WorkflowTriggerType,
|
||||||
} from 'src/app/data/workflow-trigger'
|
} from 'src/app/data/workflow-trigger'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
@ -47,6 +46,7 @@ import { WorkflowService } from 'src/app/services/rest/workflow.service'
|
|||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ConfirmButtonComponent } from '../../confirm-button/confirm-button.component'
|
import { ConfirmButtonComponent } from '../../confirm-button/confirm-button.component'
|
||||||
import { CheckComponent } from '../../input/check/check.component'
|
import { CheckComponent } from '../../input/check/check.component'
|
||||||
|
import { CustomFieldsSelectComponent } from '../../input/custom-fields-select/custom-fields-select.component'
|
||||||
import { EntriesComponent } from '../../input/entries/entries.component'
|
import { EntriesComponent } from '../../input/entries/entries.component'
|
||||||
import { NumberComponent } from '../../input/number/number.component'
|
import { NumberComponent } from '../../input/number/number.component'
|
||||||
import { PermissionsGroupComponent } from '../../input/permissions/permissions-group/permissions-group.component'
|
import { PermissionsGroupComponent } from '../../input/permissions/permissions-group/permissions-group.component'
|
||||||
@ -148,6 +148,7 @@ const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter(
|
|||||||
SwitchComponent,
|
SwitchComponent,
|
||||||
NumberComponent,
|
NumberComponent,
|
||||||
TextComponent,
|
TextComponent,
|
||||||
|
CustomFieldsSelectComponent,
|
||||||
SelectComponent,
|
SelectComponent,
|
||||||
TextAreaComponent,
|
TextAreaComponent,
|
||||||
TagsComponent,
|
TagsComponent,
|
||||||
@ -174,7 +175,6 @@ export class WorkflowEditDialogComponent
|
|||||||
documentTypes: DocumentType[]
|
documentTypes: DocumentType[]
|
||||||
storagePaths: StoragePath[]
|
storagePaths: StoragePath[]
|
||||||
mailRules: MailRule[]
|
mailRules: MailRule[]
|
||||||
customFields: CustomField[]
|
|
||||||
dateCustomFields: CustomField[]
|
dateCustomFields: CustomField[]
|
||||||
|
|
||||||
expandedItem: number = null
|
expandedItem: number = null
|
||||||
@ -189,8 +189,7 @@ export class WorkflowEditDialogComponent
|
|||||||
storagePathService: StoragePathService,
|
storagePathService: StoragePathService,
|
||||||
mailRuleService: MailRuleService,
|
mailRuleService: MailRuleService,
|
||||||
userService: UserService,
|
userService: UserService,
|
||||||
settingsService: SettingsService,
|
settingsService: SettingsService
|
||||||
customFieldsService: CustomFieldsService
|
|
||||||
) {
|
) {
|
||||||
super(service, activeModal, userService, settingsService)
|
super(service, activeModal, userService, settingsService)
|
||||||
|
|
||||||
@ -213,16 +212,6 @@ export class WorkflowEditDialogComponent
|
|||||||
.listAll()
|
.listAll()
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe((result) => (this.mailRules = result.results))
|
.subscribe((result) => (this.mailRules = result.results))
|
||||||
|
|
||||||
customFieldsService
|
|
||||||
.listAll()
|
|
||||||
.pipe(first())
|
|
||||||
.subscribe((result) => {
|
|
||||||
this.customFields = result.results
|
|
||||||
this.dateCustomFields = this.customFields?.filter(
|
|
||||||
(f) => f.data_type === CustomFieldDataType.Date
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateTitle() {
|
getCreateTitle() {
|
||||||
@ -263,6 +252,8 @@ export class WorkflowEditDialogComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
private checkRemovalActionFields(formWorkflow: Workflow) {
|
private checkRemovalActionFields(formWorkflow: Workflow) {
|
||||||
|
console.log('checkRemovalActionFields', formWorkflow)
|
||||||
|
|
||||||
formWorkflow.actions
|
formWorkflow.actions
|
||||||
.filter((action) => action.type === WorkflowActionType.Removal)
|
.filter((action) => action.type === WorkflowActionType.Removal)
|
||||||
.forEach((action, i) => {
|
.forEach((action, i) => {
|
||||||
@ -438,7 +429,9 @@ export class WorkflowEditDialogComponent
|
|||||||
assign_view_groups: new FormControl(action.assign_view_groups),
|
assign_view_groups: new FormControl(action.assign_view_groups),
|
||||||
assign_change_users: new FormControl(action.assign_change_users),
|
assign_change_users: new FormControl(action.assign_change_users),
|
||||||
assign_change_groups: new FormControl(action.assign_change_groups),
|
assign_change_groups: new FormControl(action.assign_change_groups),
|
||||||
assign_custom_fields: new FormControl(action.assign_custom_fields),
|
assign_custom_fields_w_values: new FormControl(
|
||||||
|
action.assign_custom_fields_w_values
|
||||||
|
),
|
||||||
remove_tags: new FormControl(action.remove_tags),
|
remove_tags: new FormControl(action.remove_tags),
|
||||||
remove_all_tags: new FormControl(action.remove_all_tags),
|
remove_all_tags: new FormControl(action.remove_all_tags),
|
||||||
remove_document_types: new FormControl(action.remove_document_types),
|
remove_document_types: new FormControl(action.remove_document_types),
|
||||||
@ -564,7 +557,7 @@ export class WorkflowEditDialogComponent
|
|||||||
assign_view_groups: [],
|
assign_view_groups: [],
|
||||||
assign_change_users: [],
|
assign_change_users: [],
|
||||||
assign_change_groups: [],
|
assign_change_groups: [],
|
||||||
assign_custom_fields: [],
|
assign_custom_fields_w_values: [],
|
||||||
remove_tags: [],
|
remove_tags: [],
|
||||||
remove_all_tags: false,
|
remove_all_tags: false,
|
||||||
remove_document_types: [],
|
remove_document_types: [],
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
<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">
|
||||||
|
@if (title) {
|
||||||
|
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div [class.col-md-9]="horizontal">
|
||||||
|
<div [class.is-invalid]="error">
|
||||||
|
<ng-select name="inputId" [(ngModel)]="selectedFields"
|
||||||
|
[disabled]="disabled"
|
||||||
|
[clearable]="true"
|
||||||
|
[items]="fields"
|
||||||
|
[addTag]="false"
|
||||||
|
notFoundText="No fields found"
|
||||||
|
i18n-notFoundText
|
||||||
|
[multiple]="true"
|
||||||
|
bindLabel="name"
|
||||||
|
bindValue="id"
|
||||||
|
(change)="onChange(value)">
|
||||||
|
<ng-template ng-option-tmp let-item="item">
|
||||||
|
<span [title]="item.name">{{item.name}}</span>
|
||||||
|
</ng-template>
|
||||||
|
</ng-select>
|
||||||
|
@if (selectedFields.length) {
|
||||||
|
<div class="list-group mt-3 selected-fields">
|
||||||
|
@for (fieldId of selectedFields; track fieldId) {
|
||||||
|
<div class="list-group-item
|
||||||
|
d-flex
|
||||||
|
justify-content-between
|
||||||
|
align-items-center">
|
||||||
|
@switch (getCustomField(fieldId)?.data_type) {
|
||||||
|
@case (CustomFieldDataType.String) {
|
||||||
|
<pngx-input-text [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||||
|
[title]="getCustomField(fieldId)?.name"
|
||||||
|
class="flex-grow-1"
|
||||||
|
[horizontal]="true"></pngx-input-text>
|
||||||
|
}
|
||||||
|
@case (CustomFieldDataType.Date) {
|
||||||
|
<pngx-input-date [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||||
|
[title]="getCustomField(fieldId)?.name"
|
||||||
|
class="flex-grow-1"
|
||||||
|
[horizontal]="true"></pngx-input-date>
|
||||||
|
}
|
||||||
|
@case (CustomFieldDataType.Integer) {
|
||||||
|
<pngx-input-number [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||||
|
[title]="getCustomField(fieldId)?.name"
|
||||||
|
class="flex-grow-1"
|
||||||
|
[horizontal]="true"
|
||||||
|
[showAdd]="false"></pngx-input-number>
|
||||||
|
}
|
||||||
|
@case (CustomFieldDataType.Float) {
|
||||||
|
<pngx-input-number [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||||
|
[title]="getCustomField(fieldId)?.name"
|
||||||
|
class="flex-grow-1"
|
||||||
|
[horizontal]="true"
|
||||||
|
[showAdd]="false"
|
||||||
|
[step]=".1"></pngx-input-number>
|
||||||
|
}
|
||||||
|
@case (CustomFieldDataType.Monetary) {
|
||||||
|
<pngx-input-monetary [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||||
|
[title]="getCustomField(fieldId)?.name"
|
||||||
|
class="flex-grow-1"
|
||||||
|
[defaultCurrency]="getCustomField(fieldId)?.extra_data?.default_currency"
|
||||||
|
class="flex-grow-1"
|
||||||
|
[horizontal]="true"></pngx-input-monetary>
|
||||||
|
}
|
||||||
|
@case (CustomFieldDataType.Boolean) {
|
||||||
|
<pngx-input-check [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||||
|
[title]="getCustomField(fieldId)?.name"
|
||||||
|
class="flex-grow-1"
|
||||||
|
[horizontal]="true"></pngx-input-check>
|
||||||
|
}
|
||||||
|
@case (CustomFieldDataType.Url) {
|
||||||
|
<pngx-input-url [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||||
|
[title]="getCustomField(fieldId)?.name"
|
||||||
|
class="flex-grow-1"
|
||||||
|
[horizontal]="true"></pngx-input-url>
|
||||||
|
}
|
||||||
|
@case (CustomFieldDataType.DocumentLink) {
|
||||||
|
<pngx-input-document-link [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||||
|
[title]="getCustomField(fieldId)?.name"
|
||||||
|
class="flex-grow-1"
|
||||||
|
[horizontal]="true"></pngx-input-document-link>
|
||||||
|
}
|
||||||
|
@case (CustomFieldDataType.Select) {
|
||||||
|
<pngx-input-select [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||||
|
[title]="getCustomField(fieldId)?.name"
|
||||||
|
class="flex-grow-1"
|
||||||
|
[items]="getCustomField(fieldId)?.extra_data.select_options"
|
||||||
|
class="flex-grow-1"
|
||||||
|
bindLabel="label"
|
||||||
|
[allowNull]="true"
|
||||||
|
[horizontal]="true"></pngx-input-select>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<button type="button" class="btn btn-link text-danger" (click)="removeField(fieldId)">
|
||||||
|
<i-bs name="trash"></i-bs>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{error}}
|
||||||
|
</div>
|
||||||
|
@if (hint) {
|
||||||
|
<small class="form-text text-muted">{{hint}}</small>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,41 @@
|
|||||||
|
// styles for ng-select child are in styles.scss
|
||||||
|
.paperless-input-select.disabled {
|
||||||
|
.input-group,
|
||||||
|
div > div {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep ng-select {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.ng-select-container {
|
||||||
|
background-color: var(--pngx-bg-disabled) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .private .ng-value-container {
|
||||||
|
font-style: italic;
|
||||||
|
opacity: .75;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .is-invalid ng-select .ng-select-container input {
|
||||||
|
// replicate bootstrap
|
||||||
|
padding-right: calc(1.5em + 0.75rem) !important;
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
|
background-position: right calc(0.375em + 0.1875rem) center !important;
|
||||||
|
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group .ng-select-taggable:first-child:nth-last-child(2) {
|
||||||
|
max-width: calc(100% - 45px); // fudge factor for (1x) ng-select button width
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group .ng-select-taggable:first-child:nth-last-child(3) {
|
||||||
|
max-width: calc(100% - 90px); // fudge factor for (2x) ng-select button width
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .list-group-item .mb-3 {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
import {
|
||||||
|
ComponentFixture,
|
||||||
|
TestBed,
|
||||||
|
fakeAsync,
|
||||||
|
tick,
|
||||||
|
} from '@angular/core/testing'
|
||||||
|
import {
|
||||||
|
FormsModule,
|
||||||
|
NG_VALUE_ACCESSOR,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
} from '@angular/forms'
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
|
import {
|
||||||
|
DEFAULT_MATCHING_ALGORITHM,
|
||||||
|
MATCH_ALL,
|
||||||
|
} from 'src/app/data/matching-model'
|
||||||
|
import { Tag } from 'src/app/data/tag'
|
||||||
|
import { SelectComponent } from './select.component'
|
||||||
|
|
||||||
|
const items: Tag[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Tag1',
|
||||||
|
is_inbox_tag: false,
|
||||||
|
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Tag2',
|
||||||
|
is_inbox_tag: true,
|
||||||
|
matching_algorithm: MATCH_ALL,
|
||||||
|
match: 'str',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
name: 'Tag10',
|
||||||
|
is_inbox_tag: false,
|
||||||
|
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
describe('SelectComponent', () => {
|
||||||
|
let component: SelectComponent
|
||||||
|
let fixture: ComponentFixture<SelectComponent>
|
||||||
|
let input: HTMLInputElement
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [],
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgSelectModule,
|
||||||
|
RouterTestingModule,
|
||||||
|
SelectComponent,
|
||||||
|
],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SelectComponent)
|
||||||
|
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support private items', () => {
|
||||||
|
component.value = 3
|
||||||
|
component.items = items
|
||||||
|
expect(component.items).toContainEqual({
|
||||||
|
id: 3,
|
||||||
|
name: 'Private',
|
||||||
|
private: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
component.checkForPrivateItems([4, 5])
|
||||||
|
expect(component.items).toContainEqual({
|
||||||
|
id: 4,
|
||||||
|
name: 'Private',
|
||||||
|
private: true,
|
||||||
|
})
|
||||||
|
expect(component.items).toContainEqual({
|
||||||
|
id: 5,
|
||||||
|
name: 'Private',
|
||||||
|
private: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support suggestions', () => {
|
||||||
|
expect(component.value).toBeUndefined()
|
||||||
|
component.items = items
|
||||||
|
component.suggestions = [1, 2]
|
||||||
|
fixture.detectChanges()
|
||||||
|
const suggestionAnchor: HTMLAnchorElement =
|
||||||
|
fixture.nativeElement.querySelector('a')
|
||||||
|
suggestionAnchor.click()
|
||||||
|
expect(component.value).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support create new and emit the value', () => {
|
||||||
|
expect(component.allowCreateNew).toBeFalsy()
|
||||||
|
component.items = items
|
||||||
|
let createNewVal
|
||||||
|
component.createNew.subscribe((v) => (createNewVal = v))
|
||||||
|
expect(component.allowCreateNew).toBeTruthy()
|
||||||
|
component.onSearch({ term: 'foo' })
|
||||||
|
component.addItem(undefined)
|
||||||
|
expect(createNewVal).toEqual('foo')
|
||||||
|
component.addItem('bar')
|
||||||
|
expect(createNewVal).toEqual('bar')
|
||||||
|
component.onSearch({ term: 'baz' })
|
||||||
|
component.clickNew()
|
||||||
|
expect(createNewVal).toEqual('baz')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should clear search term on blur after delay', fakeAsync(() => {
|
||||||
|
const clearSpy = jest.spyOn(component, 'clearLastSearchTerm')
|
||||||
|
component.onBlur()
|
||||||
|
tick(3000)
|
||||||
|
expect(clearSpy).toHaveBeenCalled()
|
||||||
|
}))
|
||||||
|
|
||||||
|
it('should emit filtered documents', () => {
|
||||||
|
component.value = 10
|
||||||
|
component.items = items
|
||||||
|
const emitSpy = jest.spyOn(component.filterDocuments, 'emit')
|
||||||
|
component.onFilterDocuments()
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith([items[2]])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the correct filter button title', () => {
|
||||||
|
component.title = 'Tag'
|
||||||
|
const expectedTitle = `Filter documents with this ${component.title}`
|
||||||
|
expect(component.filterButtonTitle).toEqual(expectedTitle)
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,92 @@
|
|||||||
|
import { Component, forwardRef } from '@angular/core'
|
||||||
|
import {
|
||||||
|
FormsModule,
|
||||||
|
NG_VALUE_ACCESSOR,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
} from '@angular/forms'
|
||||||
|
import { RouterModule } from '@angular/router'
|
||||||
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
|
import { AbstractInputComponent } from '../abstract-input'
|
||||||
|
import { CheckComponent } from '../check/check.component'
|
||||||
|
import { DateComponent } from '../date/date.component'
|
||||||
|
import { DocumentLinkComponent } from '../document-link/document-link.component'
|
||||||
|
import { MonetaryComponent } from '../monetary/monetary.component'
|
||||||
|
import { NumberComponent } from '../number/number.component'
|
||||||
|
import { SelectComponent } from '../select/select.component'
|
||||||
|
import { TextComponent } from '../text/text.component'
|
||||||
|
import { UrlComponent } from '../url/url.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => CustomFieldsSelectComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selector: 'pngx-input-custom-fields-select',
|
||||||
|
templateUrl: './custom-fields-select.component.html',
|
||||||
|
styleUrls: ['./custom-fields-select.component.scss'],
|
||||||
|
imports: [
|
||||||
|
TextComponent,
|
||||||
|
DateComponent,
|
||||||
|
NumberComponent,
|
||||||
|
DocumentLinkComponent,
|
||||||
|
UrlComponent,
|
||||||
|
SelectComponent,
|
||||||
|
MonetaryComponent,
|
||||||
|
CheckComponent,
|
||||||
|
NgSelectModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CustomFieldsSelectComponent extends AbstractInputComponent<Object> {
|
||||||
|
public CustomFieldDataType = CustomFieldDataType
|
||||||
|
|
||||||
|
constructor(customFieldsService: CustomFieldsService) {
|
||||||
|
super()
|
||||||
|
customFieldsService.listAll().subscribe((items) => {
|
||||||
|
this.fields = items.results
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fields: CustomField[]
|
||||||
|
|
||||||
|
_selectedFields: number[]
|
||||||
|
set selectedFields(newFields: number[]) {
|
||||||
|
this._selectedFields = newFields
|
||||||
|
// map the selected fields to an object with field_id as key and value as value
|
||||||
|
this.value = newFields.reduce((acc, fieldId) => {
|
||||||
|
acc[fieldId] = this.value?.[fieldId] || null
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
this.onChange(this.value)
|
||||||
|
}
|
||||||
|
get selectedFields(): number[] {
|
||||||
|
return this._selectedFields
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(newValue: Object): void {
|
||||||
|
// value will be a json object with field_id as key and value as value
|
||||||
|
this._selectedFields = newValue
|
||||||
|
? this.fields
|
||||||
|
.filter((field) => field.id in newValue)
|
||||||
|
.map((field) => field.id)
|
||||||
|
: []
|
||||||
|
super.writeValue(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCustomField(id: number): CustomField {
|
||||||
|
return this.fields.find((field) => field.id === id)
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeField(fieldId: number): void {
|
||||||
|
this.selectedFields = this.selectedFields.filter((id) => id !== fieldId)
|
||||||
|
}
|
||||||
|
}
|
@ -56,7 +56,7 @@ export interface WorkflowAction extends ObjectWithId {
|
|||||||
|
|
||||||
assign_change_groups?: number[] // [Group.id]
|
assign_change_groups?: number[] // [Group.id]
|
||||||
|
|
||||||
assign_custom_fields?: number[] // [CustomField.id]
|
assign_custom_fields_w_values?: number[] // { [CustomField.id]: value }
|
||||||
|
|
||||||
remove_tags?: number[] // Tag.id
|
remove_tags?: number[] // Tag.id
|
||||||
|
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-03-01 04:49
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
import documents.models
|
||||||
|
|
||||||
|
|
||||||
|
def convert_assign_custom_fields(apps, schema_editor):
|
||||||
|
# Convert the old assign_custom_fields ManyToManyField to the new assign_custom_fields_w_values JSONField
|
||||||
|
WorkflowAction = apps.get_model("documents", "WorkflowAction")
|
||||||
|
for workflow_action in WorkflowAction.objects.all():
|
||||||
|
if workflow_action.assign_custom_fields.exists():
|
||||||
|
workflow_action.assign_custom_fields_w_values = {
|
||||||
|
custom_field.id: None
|
||||||
|
for custom_field in workflow_action.assign_custom_fields.all()
|
||||||
|
}
|
||||||
|
workflow_action.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("documents", "1063_paperlesstask_type_alter_paperlesstask_task_name_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="assign_custom_fields_w_values",
|
||||||
|
field=models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
help_text="assign these custom fields, with optional values",
|
||||||
|
null=True,
|
||||||
|
verbose_name=documents.models.CustomField,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(convert_assign_custom_fields, migrations.RunPython.noop),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="assign_custom_fields",
|
||||||
|
),
|
||||||
|
]
|
@ -1264,11 +1264,13 @@ class WorkflowAction(models.Model):
|
|||||||
verbose_name=_("grant change permissions to these groups"),
|
verbose_name=_("grant change permissions to these groups"),
|
||||||
)
|
)
|
||||||
|
|
||||||
assign_custom_fields = models.ManyToManyField(
|
assign_custom_fields_w_values = models.JSONField(
|
||||||
CustomField,
|
CustomField,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="+",
|
null=True,
|
||||||
verbose_name=_("assign these custom fields"),
|
help_text=_(
|
||||||
|
"assign these custom fields, with optional values",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
remove_tags = models.ManyToManyField(
|
remove_tags = models.ManyToManyField(
|
||||||
|
@ -2017,7 +2017,7 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
|
|||||||
"assign_view_groups",
|
"assign_view_groups",
|
||||||
"assign_change_users",
|
"assign_change_users",
|
||||||
"assign_change_groups",
|
"assign_change_groups",
|
||||||
"assign_custom_fields",
|
"assign_custom_fields_w_values",
|
||||||
"remove_all_tags",
|
"remove_all_tags",
|
||||||
"remove_tags",
|
"remove_tags",
|
||||||
"remove_all_correspondents",
|
"remove_all_correspondents",
|
||||||
@ -2135,7 +2135,6 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
|||||||
assign_view_groups = action.pop("assign_view_groups", None)
|
assign_view_groups = action.pop("assign_view_groups", None)
|
||||||
assign_change_users = action.pop("assign_change_users", None)
|
assign_change_users = action.pop("assign_change_users", None)
|
||||||
assign_change_groups = action.pop("assign_change_groups", None)
|
assign_change_groups = action.pop("assign_change_groups", None)
|
||||||
assign_custom_fields = action.pop("assign_custom_fields", None)
|
|
||||||
remove_tags = action.pop("remove_tags", None)
|
remove_tags = action.pop("remove_tags", None)
|
||||||
remove_correspondents = action.pop("remove_correspondents", None)
|
remove_correspondents = action.pop("remove_correspondents", None)
|
||||||
remove_document_types = action.pop("remove_document_types", None)
|
remove_document_types = action.pop("remove_document_types", None)
|
||||||
@ -2185,8 +2184,6 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
|||||||
action_instance.assign_change_users.set(assign_change_users)
|
action_instance.assign_change_users.set(assign_change_users)
|
||||||
if assign_change_groups is not None:
|
if assign_change_groups is not None:
|
||||||
action_instance.assign_change_groups.set(assign_change_groups)
|
action_instance.assign_change_groups.set(assign_change_groups)
|
||||||
if assign_custom_fields is not None:
|
|
||||||
action_instance.assign_custom_fields.set(assign_custom_fields)
|
|
||||||
if remove_tags is not None:
|
if remove_tags is not None:
|
||||||
action_instance.remove_tags.set(remove_tags)
|
action_instance.remove_tags.set(remove_tags)
|
||||||
if remove_correspondents is not None:
|
if remove_correspondents is not None:
|
||||||
|
@ -576,6 +576,8 @@ def cleanup_custom_field_deletion(sender, instance: CustomField, **kwargs):
|
|||||||
f"Removing custom field {instance} from sort field of {views_with_sort_updated} views",
|
f"Removing custom field {instance} from sort field of {views_with_sort_updated} views",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Remove from workflow actions
|
||||||
|
|
||||||
|
|
||||||
def add_to_index(sender, document, **kwargs):
|
def add_to_index(sender, document, **kwargs):
|
||||||
from documents import index
|
from documents import index
|
||||||
|
Loading…
x
Reference in New Issue
Block a user