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