1 import { NgTemplateOutlet } from '@angular/common'
11 } from '@angular/core'
12 import { FormsModule, ReactiveFormsModule } from '@angular/forms'
17 } from '@ng-bootstrap/ng-bootstrap'
18 import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
19 import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
20 import { first, Subject, takeUntil } from 'rxjs'
21 import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
23 CUSTOM_FIELD_QUERY_MAX_ATOMS,
24 CUSTOM_FIELD_QUERY_MAX_DEPTH,
25 CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE,
26 CUSTOM_FIELD_QUERY_OPERATOR_LABELS,
27 CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
28 CustomFieldQueryElementType,
29 CustomFieldQueryOperator,
30 CustomFieldQueryOperatorGroups,
31 } from 'src/app/data/custom-field-query'
32 import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
35 CustomFieldQueryElement,
36 CustomFieldQueryExpression,
37 } from 'src/app/utils/custom-field-query-element'
38 import { pngxPopperOptions } from 'src/app/utils/popper-options'
39 import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
40 import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
41 import { DocumentLinkComponent } from '../input/document-link/document-link.component'
43 export class CustomFieldQueriesModel {
44 public queries: CustomFieldQueryElement[] = []
46 public readonly changed = new Subject<CustomFieldQueriesModel>()
48 public clear(fireEvent = true) {
51 this.changed.next(this)
55 public isValid(): boolean {
57 this.queries.length > 0 &&
58 this.validateExpression(this.queries[0] as CustomFieldQueryExpression)
62 public isEmpty(): boolean {
64 this.queries.length === 0 ||
65 (this.queries.length === 1 && this.queries[0].value.length === 0)
69 private validateAtom(atom: CustomFieldQueryAtom) {
70 let valid = !!(atom.field && atom.operator && atom.value !== null)
73 CustomFieldQueryOperator.In.valueOf(),
74 CustomFieldQueryOperator.Contains.valueOf(),
75 ].includes(atom.operator) &&
78 valid = valid && atom.value.length > 0
83 private validateExpression(expression: CustomFieldQueryExpression) {
85 expression.operator &&
86 expression.value.length > 0 &&
87 (expression.value as CustomFieldQueryElement[]).every((e) =>
88 e.type === CustomFieldQueryElementType.Atom
89 ? this.validateAtom(e as CustomFieldQueryAtom)
90 : this.validateExpression(e as CustomFieldQueryExpression)
95 public addAtom(atom: CustomFieldQueryAtom) {
96 if (this.queries.length === 0) {
99 ;(this.queries[0].value as CustomFieldQueryElement[]).push(atom)
100 atom.changed.subscribe(() => {
101 if (atom.field && atom.operator && atom.value) {
102 this.changed.next(this)
107 public addExpression(
108 expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
110 if (this.queries.length > 0) {
112 (this.queries[0] as CustomFieldQueryExpression)
113 .value as CustomFieldQueryElement[]
116 this.queries.push(expression)
118 expression.changed.subscribe(() => {
119 this.changed.next(this)
124 queryElement: CustomFieldQueryElement,
126 ): CustomFieldQueryElement {
128 for (let i = 0; i < elements.length; i++) {
129 if (elements[i] === queryElement) {
130 foundElement = elements.splice(i, 1)[0]
131 } else if (elements[i].type === CustomFieldQueryElementType.Expression) {
132 foundElement = this.findElement(
134 elements[i].value as CustomFieldQueryElement[]
144 public removeElement(queryElement: CustomFieldQueryElement) {
146 for (let i = 0; i < this.queries.length; i++) {
147 let query = this.queries[i]
148 if (query === queryElement) {
149 foundComponent = this.queries.splice(i, 1)[0]
151 } else if (query.type === CustomFieldQueryElementType.Expression) {
152 foundComponent = this.findElement(queryElement, query.value as any[])
155 if (foundComponent) {
156 foundComponent.changed.complete()
157 if (this.isEmpty()) {
160 this.changed.next(this)
166 selector: 'pngx-custom-fields-query-dropdown',
167 templateUrl: './custom-fields-query-dropdown.component.html',
168 styleUrls: ['./custom-fields-query-dropdown.component.scss'],
170 ClearableBadgeComponent,
172 DocumentLinkComponent,
177 NgxBootstrapIconsModule,
181 export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPermissions {
182 protected customFieldsService = inject(CustomFieldsService)
184 public CustomFieldQueryComponentType = CustomFieldQueryElementType
185 public CustomFieldQueryOperator = CustomFieldQueryOperator
186 public CustomFieldDataType = CustomFieldDataType
187 public CUSTOM_FIELD_QUERY_MAX_DEPTH = CUSTOM_FIELD_QUERY_MAX_DEPTH
188 public CUSTOM_FIELD_QUERY_MAX_ATOMS = CUSTOM_FIELD_QUERY_MAX_ATOMS
189 public popperOptions = pngxPopperOptions
195 filterPlaceholder: string = ''
201 allowSelectNone: boolean = false
210 return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
214 disabled: boolean = false
216 @ViewChild('dropdown') dropdown: NgbDropdown
218 @ViewChildren(NgSelectComponent) fieldSelects!: QueryList<NgSelectComponent>
220 private _selectionModel: CustomFieldQueriesModel
223 set selectionModel(model: CustomFieldQueriesModel) {
224 if (this._selectionModel) {
225 this._selectionModel.changed.complete()
227 model.changed.subscribe(() => {
230 this._selectionModel = model
233 get selectionModel(): CustomFieldQueriesModel {
234 return this._selectionModel
237 private onModelChange() {
238 if (this.selectionModel.isEmpty() || this.selectionModel.isValid()) {
239 this.selectionModelChange.next(this.selectionModel)
240 this.selectionModel.isEmpty() && this.dropdown?.close()
245 selectionModelChange = new EventEmitter<CustomFieldQueriesModel>()
247 customFields: CustomField[] = []
249 public readonly today: string = new Date().toISOString().split('T')[0]
253 this.selectionModel = new CustomFieldQueriesModel()
258 public onOpenChange(open: boolean) {
260 if (this.selectionModel.queries.length === 0) {
261 this.selectionModel.addAtom(
262 new CustomFieldQueryAtom([
264 CustomFieldQueryOperator.Exists,
270 this.selectionModel.queries.length === 1 &&
272 (this.selectionModel.queries[0] as CustomFieldQueryExpression)
273 ?.value[0] as CustomFieldQueryAtom
277 this.fieldSelects.first?.focus()
283 public get isActive(): boolean {
284 return this.selectionModel.isValid()
287 private getFields() {
288 this.customFieldsService
290 .pipe(first(), takeUntil(this.unsubscribeNotifier))
291 .subscribe((result) => {
292 this.customFields = result.results
296 public getCustomFieldByID(id: number): CustomField {
297 return this.customFields.find((field) => field.id === id)
300 public addAtom(expression: CustomFieldQueryExpression) {
304 public addExpression(expression: CustomFieldQueryExpression) {
305 expression.addExpression()
308 public removeElement(element: CustomFieldQueryElement) {
309 this.selectionModel.removeElement(element)
313 this.selectionModel.clear(false)
314 this.selectionModel.changed.next(this.selectionModel)
317 getOperatorsForField(
319 ): Array<{ value: string; label: string }> {
320 const field = this.customFields.find((field) => field.id === fieldID)
321 const groups: CustomFieldQueryOperatorGroups[] = field
322 ? CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE[field.data_type]
323 : [CustomFieldQueryOperatorGroups.Basic]
324 const operators = groups.flatMap(
325 (group) => CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[group]
327 return operators.map((operator) => ({
329 label: CUSTOM_FIELD_QUERY_OPERATOR_LABELS[operator],
333 getSelectOptionsForField(
335 ): Array<{ label: string; id: string }> {
336 const field = this.customFields.find((field) => field.id === fieldID)
338 return field.extra_data['select_options']