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