diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html
index add7878f4..048a04798 100644
--- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html
+++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html
@@ -188,7 +188,7 @@
-
+
diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
index 5facc5cce..b9ffa1506 100644
--- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
+++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
@@ -16,7 +16,7 @@ import { NgbAccordionModule, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { first } from 'rxjs'
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 { MailRule } from 'src/app/data/mail-rule'
import {
@@ -38,7 +38,6 @@ import {
WorkflowTriggerType,
} from 'src/app/data/workflow-trigger'
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 { MailRuleService } from 'src/app/services/rest/mail-rule.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 { ConfirmButtonComponent } from '../../confirm-button/confirm-button.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 { NumberComponent } from '../../input/number/number.component'
import { PermissionsGroupComponent } from '../../input/permissions/permissions-group/permissions-group.component'
@@ -148,6 +148,7 @@ const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter(
SwitchComponent,
NumberComponent,
TextComponent,
+ CustomFieldsSelectComponent,
SelectComponent,
TextAreaComponent,
TagsComponent,
@@ -174,7 +175,6 @@ export class WorkflowEditDialogComponent
documentTypes: DocumentType[]
storagePaths: StoragePath[]
mailRules: MailRule[]
- customFields: CustomField[]
dateCustomFields: CustomField[]
expandedItem: number = null
@@ -189,8 +189,7 @@ export class WorkflowEditDialogComponent
storagePathService: StoragePathService,
mailRuleService: MailRuleService,
userService: UserService,
- settingsService: SettingsService,
- customFieldsService: CustomFieldsService
+ settingsService: SettingsService
) {
super(service, activeModal, userService, settingsService)
@@ -213,16 +212,6 @@ export class WorkflowEditDialogComponent
.listAll()
.pipe(first())
.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() {
@@ -263,6 +252,8 @@ export class WorkflowEditDialogComponent
}
private checkRemovalActionFields(formWorkflow: Workflow) {
+ console.log('checkRemovalActionFields', formWorkflow)
+
formWorkflow.actions
.filter((action) => action.type === WorkflowActionType.Removal)
.forEach((action, i) => {
@@ -438,7 +429,9 @@ export class WorkflowEditDialogComponent
assign_view_groups: new FormControl(action.assign_view_groups),
assign_change_users: new FormControl(action.assign_change_users),
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_all_tags: new FormControl(action.remove_all_tags),
remove_document_types: new FormControl(action.remove_document_types),
@@ -564,7 +557,7 @@ export class WorkflowEditDialogComponent
assign_view_groups: [],
assign_change_users: [],
assign_change_groups: [],
- assign_custom_fields: [],
+ assign_custom_fields_w_values: [],
remove_tags: [],
remove_all_tags: false,
remove_document_types: [],
diff --git a/src-ui/src/app/components/common/input/custom-fields-select/custom-fields-select.component.html b/src-ui/src/app/components/common/input/custom-fields-select/custom-fields-select.component.html
new file mode 100644
index 000000000..3c42a59bc
--- /dev/null
+++ b/src-ui/src/app/components/common/input/custom-fields-select/custom-fields-select.component.html
@@ -0,0 +1,113 @@
+
diff --git a/src-ui/src/app/components/common/input/custom-fields-select/custom-fields-select.component.scss b/src-ui/src/app/components/common/input/custom-fields-select/custom-fields-select.component.scss
new file mode 100644
index 000000000..4141a6ca1
--- /dev/null
+++ b/src-ui/src/app/components/common/input/custom-fields-select/custom-fields-select.component.scss
@@ -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;
+}
diff --git a/src-ui/src/app/components/common/input/custom-fields-select/custom-fields-select.component.spec.ts b/src-ui/src/app/components/common/input/custom-fields-select/custom-fields-select.component.spec.ts
new file mode 100644
index 000000000..d93703ab1
--- /dev/null
+++ b/src-ui/src/app/components/common/input/custom-fields-select/custom-fields-select.component.spec.ts
@@ -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
+ 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)
+ })
+})
diff --git a/src-ui/src/app/components/common/input/custom-fields-select/custom-fields-select.component.ts b/src-ui/src/app/components/common/input/custom-fields-select/custom-fields-select.component.ts
new file mode 100644
index 000000000..8ff82370f
--- /dev/null
+++ b/src-ui/src/app/components/common/input/custom-fields-select/custom-fields-select.component.ts
@@ -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