]> git.ipfire.org Git - thirdparty/paperless-ngx.git/blob
d6ae95604ec57f326612eacc0117166ca8e098d6
[thirdparty/paperless-ngx.git] /
1 import { NgTemplateOutlet } from '@angular/common'
2 import {
3 Component,
4 EventEmitter,
5 Input,
6 Output,
7 QueryList,
8 ViewChild,
9 ViewChildren,
10 } from '@angular/core'
11 import { FormsModule, ReactiveFormsModule } from '@angular/forms'
12 import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
13 import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
14 import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
15 import { first, Subject, takeUntil } from 'rxjs'
16 import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
17 import {
18 CUSTOM_FIELD_QUERY_MAX_ATOMS,
19 CUSTOM_FIELD_QUERY_MAX_DEPTH,
20 CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE,
21 CUSTOM_FIELD_QUERY_OPERATOR_LABELS,
22 CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
23 CustomFieldQueryElementType,
24 CustomFieldQueryOperator,
25 CustomFieldQueryOperatorGroups,
26 } from 'src/app/data/custom-field-query'
27 import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
28 import {
29 CustomFieldQueryAtom,
30 CustomFieldQueryElement,
31 CustomFieldQueryExpression,
32 } from 'src/app/utils/custom-field-query-element'
33 import { popperOptionsReenablePreventOverflow } from 'src/app/utils/popper-options'
34 import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
35 import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
36
37 export class CustomFieldQueriesModel {
38 public queries: CustomFieldQueryElement[] = []
39
40 public readonly changed = new Subject<CustomFieldQueriesModel>()
41
42 public clear(fireEvent = true) {
43 this.queries = []
44 if (fireEvent) {
45 this.changed.next(this)
46 }
47 }
48
49 public isValid(): boolean {
50 return (
51 this.queries.length > 0 &&
52 this.validateExpression(this.queries[0] as CustomFieldQueryExpression)
53 )
54 }
55
56 public isEmpty(): boolean {
57 return (
58 this.queries.length === 0 ||
59 (this.queries.length === 1 && this.queries[0].value.length === 0)
60 )
61 }
62
63 private validateAtom(atom: CustomFieldQueryAtom) {
64 let valid = !!(atom.field && atom.operator && atom.value !== null)
65 if (
66 [
67 CustomFieldQueryOperator.In.valueOf(),
68 CustomFieldQueryOperator.Contains.valueOf(),
69 ].includes(atom.operator) &&
70 atom.value
71 ) {
72 valid = valid && atom.value.length > 0
73 }
74 return valid
75 }
76
77 private validateExpression(expression: CustomFieldQueryExpression) {
78 return (
79 expression.operator &&
80 expression.value.length > 0 &&
81 (expression.value as CustomFieldQueryElement[]).every((e) =>
82 e.type === CustomFieldQueryElementType.Atom
83 ? this.validateAtom(e as CustomFieldQueryAtom)
84 : this.validateExpression(e as CustomFieldQueryExpression)
85 )
86 )
87 }
88
89 public addAtom(atom: CustomFieldQueryAtom) {
90 if (this.queries.length === 0) {
91 this.addExpression()
92 }
93 ;(this.queries[0].value as CustomFieldQueryElement[]).push(atom)
94 atom.changed.subscribe(() => {
95 if (atom.field && atom.operator && atom.value) {
96 this.changed.next(this)
97 }
98 })
99 }
100
101 public addExpression(
102 expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
103 ) {
104 if (this.queries.length > 0) {
105 ;(
106 (this.queries[0] as CustomFieldQueryExpression)
107 .value as CustomFieldQueryElement[]
108 ).push(expression)
109 } else {
110 this.queries.push(expression)
111 }
112 expression.changed.subscribe(() => {
113 this.changed.next(this)
114 })
115 }
116
117 private findElement(
118 queryElement: CustomFieldQueryElement,
119 elements: any[]
120 ): CustomFieldQueryElement {
121 let foundElement
122 for (let i = 0; i < elements.length; i++) {
123 if (elements[i] === queryElement) {
124 foundElement = elements.splice(i, 1)[0]
125 } else if (elements[i].type === CustomFieldQueryElementType.Expression) {
126 foundElement = this.findElement(
127 queryElement,
128 elements[i].value as CustomFieldQueryElement[]
129 )
130 }
131 if (foundElement) {
132 break
133 }
134 }
135 return foundElement
136 }
137
138 public removeElement(queryElement: CustomFieldQueryElement) {
139 let foundComponent
140 for (let i = 0; i < this.queries.length; i++) {
141 let query = this.queries[i]
142 if (query === queryElement) {
143 foundComponent = this.queries.splice(i, 1)[0]
144 break
145 } else if (query.type === CustomFieldQueryElementType.Expression) {
146 foundComponent = this.findElement(queryElement, query.value as any[])
147 }
148 }
149 if (foundComponent) {
150 foundComponent.changed.complete()
151 if (this.isEmpty()) {
152 this.clear()
153 }
154 this.changed.next(this)
155 }
156 }
157 }
158
159 @Component({
160 selector: 'pngx-custom-fields-query-dropdown',
161 templateUrl: './custom-fields-query-dropdown.component.html',
162 styleUrls: ['./custom-fields-query-dropdown.component.scss'],
163 imports: [
164 ClearableBadgeComponent,
165 FormsModule,
166 ReactiveFormsModule,
167 NgTemplateOutlet,
168 NgSelectModule,
169 NgxBootstrapIconsModule,
170 NgbDropdownModule,
171 ],
172 })
173 export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPermissions {
174 public CustomFieldQueryComponentType = CustomFieldQueryElementType
175 public CustomFieldQueryOperator = CustomFieldQueryOperator
176 public CustomFieldDataType = CustomFieldDataType
177 public CUSTOM_FIELD_QUERY_MAX_DEPTH = CUSTOM_FIELD_QUERY_MAX_DEPTH
178 public CUSTOM_FIELD_QUERY_MAX_ATOMS = CUSTOM_FIELD_QUERY_MAX_ATOMS
179 public popperOptions = popperOptionsReenablePreventOverflow
180
181 @Input()
182 title: string
183
184 @Input()
185 filterPlaceholder: string = ''
186
187 @Input()
188 icon: string
189
190 @Input()
191 allowSelectNone: boolean = false
192
193 @Input()
194 editing = false
195
196 @Input()
197 applyOnClose = false
198
199 get name(): string {
200 return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
201 }
202
203 @Input()
204 disabled: boolean = false
205
206 @ViewChild('dropdown') dropdown: NgbDropdown
207
208 @ViewChildren(NgSelectComponent) fieldSelects!: QueryList<NgSelectComponent>
209
210 private _selectionModel: CustomFieldQueriesModel
211
212 @Input()
213 set selectionModel(model: CustomFieldQueriesModel) {
214 if (this._selectionModel) {
215 this._selectionModel.changed.complete()
216 }
217 model.changed.subscribe(() => {
218 this.onModelChange()
219 })
220 this._selectionModel = model
221 }
222
223 get selectionModel(): CustomFieldQueriesModel {
224 return this._selectionModel
225 }
226
227 private onModelChange() {
228 if (this.selectionModel.isEmpty() || this.selectionModel.isValid()) {
229 this.selectionModelChange.next(this.selectionModel)
230 this.selectionModel.isEmpty() && this.dropdown?.close()
231 }
232 }
233
234 @Output()
235 selectionModelChange = new EventEmitter<CustomFieldQueriesModel>()
236
237 customFields: CustomField[] = []
238
239 constructor(protected customFieldsService: CustomFieldsService) {
240 super()
241 this.selectionModel = new CustomFieldQueriesModel()
242 this.getFields()
243 this.reset()
244 }
245
246 public onOpenChange(open: boolean) {
247 if (open) {
248 if (this.selectionModel.queries.length === 0) {
249 this.selectionModel.addAtom(
250 new CustomFieldQueryAtom([
251 null,
252 CustomFieldQueryOperator.Exists,
253 'true',
254 ])
255 )
256 }
257 if (
258 this.selectionModel.queries.length === 1 &&
259 (
260 (this.selectionModel.queries[0] as CustomFieldQueryExpression)
261 ?.value[0] as CustomFieldQueryAtom
262 )?.field === null
263 ) {
264 setTimeout(() => {
265 this.fieldSelects.first?.focus()
266 }, 0)
267 }
268 }
269 }
270
271 public get isActive(): boolean {
272 return this.selectionModel.isValid()
273 }
274
275 private getFields() {
276 this.customFieldsService
277 .listAll()
278 .pipe(first(), takeUntil(this.unsubscribeNotifier))
279 .subscribe((result) => {
280 this.customFields = result.results
281 })
282 }
283
284 public getCustomFieldByID(id: number): CustomField {
285 return this.customFields.find((field) => field.id === id)
286 }
287
288 public addAtom(expression: CustomFieldQueryExpression) {
289 expression.addAtom()
290 }
291
292 public addExpression(expression: CustomFieldQueryExpression) {
293 expression.addExpression()
294 }
295
296 public removeElement(element: CustomFieldQueryElement) {
297 this.selectionModel.removeElement(element)
298 }
299
300 public reset() {
301 this.selectionModel.clear(false)
302 this.selectionModel.changed.next(this.selectionModel)
303 }
304
305 getOperatorsForField(
306 fieldID: number
307 ): Array<{ value: string; label: string }> {
308 const field = this.customFields.find((field) => field.id === fieldID)
309 const groups: CustomFieldQueryOperatorGroups[] = field
310 ? CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE[field.data_type]
311 : [CustomFieldQueryOperatorGroups.Basic]
312 const operators = groups.flatMap(
313 (group) => CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[group]
314 )
315 return operators.map((operator) => ({
316 value: operator,
317 label: CUSTOM_FIELD_QUERY_OPERATOR_LABELS[operator],
318 }))
319 }
320
321 getSelectOptionsForField(
322 fieldID: number
323 ): Array<{ label: string; id: string }> {
324 const field = this.customFields.find((field) => field.id === fieldID)
325 if (field) {
326 return field.extra_data['select_options']
327 }
328 return []
329 }
330 }