Finding from the review at ece67d3.
dynamicWhereClauseCreator's IN branch mapped the entry through element.values
only when building the literal; the two fallbacks emitted the unmapped value, so
a formatted label could still reach RAW_VALUE. A temporal entry whose mapped
value is not a whole day / second (sasTemporalLiteral returns null for a
genuinely fractional value) or whose label finds no match in element.values
would fall through to the quoted label on a numeric column, which mp_filtercheck
rejects and which aborts the getcolvals load inside the swallowed catch.
The mapped value is now assigned once and used in all three branches, as
createFilterQueryTable's IN branch does.
One unit case added for the non-literal fallback. 582/582 unit tests pass.
1125 lines
32 KiB
TypeScript
1125 lines
32 KiB
TypeScript
import { Subscription } from 'rxjs'
|
|
import {
|
|
Component,
|
|
AfterViewInit,
|
|
AfterContentInit,
|
|
OnDestroy,
|
|
ChangeDetectorRef,
|
|
LOCALE_ID,
|
|
Input,
|
|
ViewEncapsulation
|
|
} from '@angular/core'
|
|
import { SasStoreService } from '../services/sas-store.service'
|
|
import { globals } from '../_globals'
|
|
import { EventService } from '../services/event.service'
|
|
|
|
import { registerLocaleData } from '@angular/common'
|
|
import localeEnGB from '@angular/common/locales/en-GB'
|
|
import { HelperService } from '../services/helper.service'
|
|
import { QueryClause } from '../models/TableData'
|
|
import { PublicGetcolvalsSASResponse } from '../models/sas/public-getcolvals.model'
|
|
import { QueryDateTime } from './models/QueryDateTime.model'
|
|
import { isSpecialMissing } from '@sasjs/utils/input/validators'
|
|
import { get } from 'lodash-es'
|
|
import { OnLoadingMoreEvent } from '../shared/autocomplete/autocomplete.component'
|
|
import { getFilterObjPath } from './utils/getFilterObjPath'
|
|
|
|
registerLocaleData(localeEnGB)
|
|
@Component({
|
|
selector: 'app-query',
|
|
templateUrl: './query.component.html',
|
|
styleUrls: ['./query.component.scss'],
|
|
providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }],
|
|
encapsulation: ViewEncapsulation.None,
|
|
standalone: false
|
|
})
|
|
export class QueryComponent
|
|
implements AfterViewInit, AfterContentInit, OnDestroy
|
|
{
|
|
@Input() caching: boolean = true
|
|
@Input() viewboxId: number | undefined
|
|
|
|
private rows_increment: number = 100
|
|
private initial_rows: number = 1000
|
|
|
|
private columnsSub: Subscription | undefined
|
|
private valuesSub: Subscription | undefined
|
|
private _removeQry: Subscription | undefined
|
|
public cols: Array<any> | undefined
|
|
public libds: string | undefined
|
|
public variable: string | undefined
|
|
public value: string | undefined
|
|
public operator: string | undefined
|
|
public vals: Array<any> | undefined
|
|
public innerWidth: any
|
|
public noBorder: any
|
|
public clauseAmt: Array<any> = ['']
|
|
public numOperators: Array<any> = [
|
|
'=',
|
|
'<',
|
|
'>',
|
|
'<=',
|
|
'>=',
|
|
'BETWEEN',
|
|
'IN',
|
|
'NOT IN',
|
|
'NE'
|
|
]
|
|
public charOperators: Array<any> = [
|
|
'=',
|
|
'<',
|
|
'>',
|
|
'<=',
|
|
'>=',
|
|
'CONTAINS',
|
|
'IN',
|
|
'NOT IN',
|
|
'NE'
|
|
// "LIKE",
|
|
// "BEGINS_WITH",
|
|
]
|
|
public logic: string | undefined
|
|
public notIn: boolean = false
|
|
public notArr: boolean = true
|
|
public wasBetween: boolean = false
|
|
public whereClauseLoading: boolean = false
|
|
public dynamicWhereClause: boolean = true
|
|
public usePickers: boolean = false
|
|
|
|
private queryObj: {
|
|
elements: any[]
|
|
clauseLogic?: string
|
|
invalidClause?: boolean
|
|
} = {
|
|
elements: [
|
|
{
|
|
logic: null,
|
|
type: null,
|
|
ddtype: null,
|
|
variable: null,
|
|
operator: null,
|
|
value: null,
|
|
valueVariable: false,
|
|
startrow: 0,
|
|
rows: 0,
|
|
nobs: 0,
|
|
values: <
|
|
{
|
|
formatted: any
|
|
unformatted: any
|
|
}[]
|
|
>[],
|
|
operators: []
|
|
}
|
|
],
|
|
clauseLogic: '',
|
|
invalidClause: false
|
|
}
|
|
|
|
public selVar: any = {
|
|
index: null,
|
|
format: null,
|
|
type: null
|
|
}
|
|
|
|
public inObj: Array<any> = [
|
|
{
|
|
checked: false,
|
|
value: null
|
|
}
|
|
]
|
|
|
|
public groupLogic: string = 'AND'
|
|
|
|
public clauses: any = {
|
|
queryObj: [this.queryObj],
|
|
clauseLogic: [],
|
|
groupLogic: this.groupLogic
|
|
}
|
|
|
|
public whereString: string | undefined
|
|
public val: any
|
|
public whereClause: string | undefined
|
|
public logicOperators: Array<string> = ['AND', 'OR']
|
|
|
|
/**
|
|
* Temporary values array used in pickers
|
|
* because they need particular format to work
|
|
* before sending values to backend, values are parsed
|
|
*/
|
|
public queryDateTime: QueryDateTime[] = []
|
|
|
|
public currentClauseIndex: number = -1
|
|
public currentQueryIndex: number = -1
|
|
public pendingINValuesSet: {
|
|
clauseIndex: number
|
|
queryIndex: number
|
|
} | null = null
|
|
|
|
constructor(
|
|
private sasStoreService: SasStoreService,
|
|
private eventService: EventService,
|
|
private helperService: HelperService,
|
|
private cdf: ChangeDetectorRef
|
|
) {
|
|
this.innerWidth = window.screen.width
|
|
if (this.innerWidth > 768) {
|
|
this.noBorder = 'border-left: 2px solid gray;'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets and sets temporary values selected with DATETIME or TIME picker
|
|
* Those values are used for picker to work with format it lieks
|
|
* Later before sending values to backend, values are parsed
|
|
*/
|
|
getQueryDateTime(clauseIndex: number, queryIndex: number): QueryDateTime {
|
|
let existingQueryDateTime = this.queryDateTime.find(
|
|
(x) => x.clauseIndex === clauseIndex && x.queryIndex === queryIndex
|
|
)
|
|
|
|
if (!existingQueryDateTime) {
|
|
const lastIndex =
|
|
this.queryDateTime.push({
|
|
clauseIndex,
|
|
queryIndex,
|
|
date: '',
|
|
time: ''
|
|
}) - 1
|
|
|
|
existingQueryDateTime = this.queryDateTime[lastIndex]
|
|
}
|
|
|
|
return existingQueryDateTime
|
|
}
|
|
|
|
/**
|
|
* When toggling pickers feature we reset the temp picker values array
|
|
*/
|
|
usePickersChange() {
|
|
this.queryDateTime = []
|
|
if (this.usePickers) {
|
|
this.clauses.queryObj.forEach((queryObj: any) => {
|
|
queryObj.elements.forEach((element: any) => {
|
|
const isDateOrTime = ['DATETIME', 'TIME', 'DATE'].includes(
|
|
element.ddtype
|
|
)
|
|
|
|
if (isDateOrTime && element.valueVariable) {
|
|
element.value = ''
|
|
element.valueVariable = false
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets all variables used for filtering
|
|
*/
|
|
public resetFilter() {
|
|
this.whereString = undefined
|
|
this.whereClause = undefined
|
|
|
|
this.queryObj = {
|
|
elements: [
|
|
{
|
|
logic: null,
|
|
type: null,
|
|
ddtype: null,
|
|
variable: null,
|
|
operator: null,
|
|
value: null,
|
|
values: [],
|
|
operators: []
|
|
}
|
|
]
|
|
}
|
|
|
|
this.clauses = {
|
|
queryObj: [this.queryObj],
|
|
clauseLogic: [],
|
|
groupLogic: this.groupLogic
|
|
}
|
|
|
|
this.whereClauseFn(true)
|
|
}
|
|
|
|
/**
|
|
* `Globals` are used to store filtering state (variables) as a caching feature
|
|
* until browser reloads
|
|
*/
|
|
public setToGlobals() {
|
|
if (!this.caching) return
|
|
|
|
const objPath = getFilterObjPath(globals.rootParam, this.viewboxId)
|
|
|
|
get(globals, objPath).filter.groupLogic = this.groupLogic
|
|
if (typeof this.whereClause === 'string') {
|
|
get(globals, objPath).filter.whereClause = this.whereClause
|
|
}
|
|
if (typeof this.libds === 'string') {
|
|
get(globals, objPath).filter.libds = this.libds
|
|
}
|
|
get(globals, objPath).filter.clauses = this.clauses
|
|
}
|
|
|
|
/**
|
|
* `Globals` are used to store filtering state (variables) as a caching feature
|
|
* until browser reloads
|
|
*/
|
|
public getFromGlobals() {
|
|
if (!this.caching) return
|
|
|
|
const objPath = getFilterObjPath(globals.rootParam, this.viewboxId)
|
|
|
|
if (get(globals, objPath).filter.cols.length > 0) {
|
|
this.cols = JSON.parse(JSON.stringify(get(globals, objPath).filter.cols))
|
|
}
|
|
|
|
if (get(globals, objPath).filter.vals.length > 0) {
|
|
this.vals = JSON.parse(JSON.stringify(get(globals, objPath).filter.vals))
|
|
}
|
|
|
|
if (get(globals, objPath).filter.groupLogic !== '') {
|
|
this.groupLogic = get(globals, objPath).filter.groupLogic
|
|
this.whereClause = get(globals, objPath).filter.whereClause
|
|
this.libds = get(globals, objPath).filter.libds
|
|
this.clauses = get(globals, objPath).filter.clauses
|
|
this.whereClauseFn()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets filtering multiple caluses group logic (and / or)
|
|
*
|
|
* @param groupLogic to set
|
|
*/
|
|
public setGroupLogic(groupLogic: any) {
|
|
this.groupLogic = groupLogic
|
|
this.clauses.groupLogic = groupLogic
|
|
this.whereClauseFn()
|
|
|
|
this.setToGlobals()
|
|
}
|
|
|
|
public dateChange(
|
|
pickerValue: any,
|
|
query: any,
|
|
queryIndex: number,
|
|
clauseIndex: number,
|
|
queryValueIndex?: number
|
|
) {
|
|
let valueInDays = this.helperService.convertJsDateToSasDate(pickerValue)
|
|
|
|
if (query.operator === 'BETWEEN' && queryValueIndex !== undefined) {
|
|
this.clauses.queryObj[clauseIndex].elements[queryIndex].value[
|
|
queryValueIndex
|
|
] = valueInDays
|
|
}
|
|
|
|
this.setVariableValues(valueInDays, queryIndex, clauseIndex)
|
|
}
|
|
|
|
public dateTimeChange(
|
|
query: any,
|
|
queryIndex: number,
|
|
clauseIndex: number,
|
|
queryValueIndex?: number
|
|
) {
|
|
const queryDateTime = this.getQueryDateTime(clauseIndex, queryIndex)
|
|
|
|
if (queryDateTime.date === '') {
|
|
if (query.value === '') return
|
|
|
|
queryDateTime.date = query.value.split(':')[0]
|
|
}
|
|
|
|
const dateHours = parseInt(queryDateTime.time.split(':')[0])
|
|
const dateMinutes = parseInt(queryDateTime.time.split(':')[1])
|
|
const dateSeconds = parseInt(queryDateTime.time.split(':')[2]) || 0
|
|
|
|
let dateObject = new Date(queryDateTime.date)
|
|
let dateTimeObject = new Date(
|
|
dateObject.getFullYear(),
|
|
dateObject.getMonth(),
|
|
dateObject.getDate(),
|
|
dateHours,
|
|
dateMinutes,
|
|
dateSeconds
|
|
)
|
|
|
|
let valueInSeconds = this.helperService.convertJsDateToSasDate(
|
|
dateTimeObject,
|
|
'seconds'
|
|
)
|
|
|
|
if (query.operator === 'BETWEEN' && queryValueIndex !== undefined) {
|
|
this.clauses.queryObj[clauseIndex].elements[queryIndex].value[
|
|
queryValueIndex
|
|
] = valueInSeconds
|
|
}
|
|
|
|
this.setVariableValues(valueInSeconds, queryIndex, clauseIndex)
|
|
}
|
|
|
|
public timeChange(
|
|
time: any,
|
|
query: any,
|
|
ind: number,
|
|
clauseIndex: number,
|
|
queryValueIndex?: number
|
|
) {
|
|
time = time.target.value
|
|
|
|
let hours = parseInt(time.split(':')[0])
|
|
let minutes = parseInt(time.split(':')[1])
|
|
let seconds = parseInt(time.split(':')[2]) || 0
|
|
|
|
let hourSeconds = hours * 60 * 60
|
|
let minuteSeconds = minutes * 60
|
|
|
|
let inputValue = hourSeconds + minuteSeconds + seconds
|
|
|
|
if (query.operator === 'BETWEEN' && queryValueIndex !== undefined) {
|
|
this.clauses.queryObj[clauseIndex].elements[ind].value[queryValueIndex] =
|
|
inputValue
|
|
}
|
|
|
|
this.setVariableValues(inputValue, ind, clauseIndex)
|
|
}
|
|
|
|
/**
|
|
* Takes the clause object, removes current clause and makes new array (table)
|
|
* ready to be sent to SAS
|
|
*/
|
|
public dynamicWhereClauseCreator(groupIndex: number, clauseIndex: number) {
|
|
let dynamicWhereClauseTable: QueryClause[] = []
|
|
|
|
if (
|
|
this.clauses.queryObj.length > 1 ||
|
|
this.clauses.queryObj[groupIndex].elements.length > 1
|
|
) {
|
|
for (
|
|
let groupInd = 0;
|
|
groupInd < this.clauses.queryObj.length;
|
|
groupInd++
|
|
) {
|
|
for (
|
|
let clauseInd = 0;
|
|
clauseInd < this.clauses.queryObj[groupInd].elements.length;
|
|
clauseInd++
|
|
) {
|
|
if (groupInd === groupIndex && clauseInd === clauseIndex) continue
|
|
if (
|
|
[null, undefined].includes(
|
|
this.clauses.queryObj[groupInd].elements[clauseInd].variable
|
|
) ||
|
|
[null, undefined].includes(
|
|
this.clauses.queryObj[groupInd].elements[clauseInd].operator
|
|
) ||
|
|
[null, undefined].includes(
|
|
this.clauses.queryObj[groupInd].elements[clauseInd].value
|
|
)
|
|
)
|
|
continue
|
|
|
|
let rawValue = ''
|
|
const element = this.clauses.queryObj[groupInd].elements[clauseInd]
|
|
const operator = element.operator
|
|
const variable = element.variable
|
|
/* a temporal value is sent as a SAS literal (eg '20APR2042'd), the
|
|
same way the applied filter carries it */
|
|
const temporalValue = (value: any) =>
|
|
element.valueVariable
|
|
? null
|
|
: this.helperService.sasTemporalLiteral(value, element.ddtype)
|
|
/* the dropdowns and the IN modal hold the *formatted* value (the
|
|
soft-select option value, and setINValues), so a picked value is
|
|
mapped back to the unformatted one the filter needs */
|
|
const unformattedValue = (value: any) =>
|
|
(element.values || []).find((v: any) => v.formatted === value)
|
|
?.unformatted ?? value
|
|
|
|
if (operator === 'BETWEEN') {
|
|
const from = unformattedValue(element.value[0])
|
|
const to = unformattedValue(element.value[1])
|
|
rawValue = `${temporalValue(from) ?? from} AND ${
|
|
temporalValue(to) ?? to
|
|
}`
|
|
} else if (operator === 'IN' || operator === 'NOT IN') {
|
|
element.value.forEach((item: any) => {
|
|
if (item.checked) {
|
|
/* map once, and use the unformatted value in every branch - the
|
|
literal is built from it, and so are the fallbacks, so a
|
|
formatted label can never reach RAW_VALUE */
|
|
const value = unformattedValue(item.val)
|
|
const literal = temporalValue(value)
|
|
|
|
if (literal) {
|
|
rawValue += (rawValue.length > 0 ? ',' : '(') + literal
|
|
} else if (
|
|
typeof value === 'string' &&
|
|
!isSpecialMissing(value)
|
|
) {
|
|
if (rawValue.length > 0) {
|
|
rawValue += `,'${value}'`
|
|
} else {
|
|
rawValue = `('${value}'`
|
|
}
|
|
} else {
|
|
if (rawValue.length > 0) {
|
|
rawValue += `,${value}`
|
|
} else {
|
|
rawValue = `(${value}`
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if (rawValue.length > 0) {
|
|
rawValue += ')'
|
|
}
|
|
} else if (
|
|
this.clauses.queryObj[groupInd].elements[clauseInd].value === ''
|
|
) {
|
|
if (
|
|
this.clauses.queryObj[groupInd].elements[clauseInd].type ===
|
|
'char'
|
|
) {
|
|
rawValue = `' '`
|
|
} else {
|
|
rawValue = '.'
|
|
}
|
|
} else {
|
|
const literal = temporalValue(element.value)
|
|
|
|
if (literal) {
|
|
rawValue = literal
|
|
} else if (element.type === 'char') {
|
|
rawValue = `'${element.value}'`
|
|
}
|
|
}
|
|
|
|
dynamicWhereClauseTable.push({
|
|
GROUP_LOGIC: this.clauses.groupLogic,
|
|
SUBGROUP_LOGIC:
|
|
this.clauses.queryObj[groupInd].clauseLogic || 'AND',
|
|
SUBGROUP_ID: groupInd,
|
|
VARIABLE_NM: variable,
|
|
OPERATOR_NM: operator,
|
|
RAW_VALUE:
|
|
rawValue ||
|
|
this.clauses.queryObj[groupInd].elements[clauseInd].value
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return dynamicWhereClauseTable
|
|
}
|
|
|
|
public async getValuesLoadMore(
|
|
event: OnLoadingMoreEvent,
|
|
variable: any,
|
|
ind: any,
|
|
clauseIndex: any
|
|
) {
|
|
const libds = this.libds || ''
|
|
|
|
try {
|
|
if (this.cols !== undefined) {
|
|
for (let index = 0; index < this.cols.length; index++) {
|
|
let element = this.cols[index]
|
|
if (element.NAME === variable) {
|
|
this.selVar.index = index
|
|
}
|
|
}
|
|
|
|
const DDTYPE = this.cols[this.selVar.index].DDTYPE
|
|
|
|
const { startrow: currentStartrow, rows: currentRows } =
|
|
this.clauses.queryObj[clauseIndex].elements[ind]
|
|
|
|
const ROWS_INCREMENT: number = this.rows_increment
|
|
const STARTROW = currentStartrow + currentRows
|
|
|
|
let dynamicWhereClauseTable: any[] = []
|
|
|
|
if (this.dynamicWhereClause) {
|
|
dynamicWhereClauseTable = this.dynamicWhereClauseCreator(
|
|
clauseIndex,
|
|
ind
|
|
)
|
|
}
|
|
|
|
const isDateOrTime = ['DATETIME', 'TIME', 'DATE'].includes(DDTYPE)
|
|
|
|
this.sasStoreService
|
|
.getQueryValues(
|
|
variable,
|
|
libds,
|
|
dynamicWhereClauseTable,
|
|
STARTROW,
|
|
ROWS_INCREMENT
|
|
)
|
|
.then((res: PublicGetcolvalsSASResponse) => {
|
|
const values = res.vals
|
|
const { STARTROW, ROWS, NOBS } = res.meta[0]
|
|
|
|
this.clauses.queryObj[clauseIndex].elements[ind].startrow = STARTROW
|
|
this.clauses.queryObj[clauseIndex].elements[ind].rows = ROWS
|
|
this.clauses.queryObj[clauseIndex].elements[ind].nobs = NOBS
|
|
|
|
if (
|
|
!(
|
|
values.length === 1 &&
|
|
values[0].FORMATTED === '' &&
|
|
values[0].UNFORMATTED === ''
|
|
)
|
|
) {
|
|
this.clauses.queryObj[clauseIndex].elements[ind].values.push(
|
|
...values.map((item: any) => {
|
|
const numeric = res.$vals.vars.UNFORMATTED.type === 'num'
|
|
const specialMissing =
|
|
numeric && isSpecialMissing(item.UNFORMATTED)
|
|
|
|
let unformatted = specialMissing
|
|
? '.' + item.UNFORMATTED
|
|
: item.UNFORMATTED
|
|
|
|
// If it's not special missing and is numeric and null then it is normal missing `.`
|
|
if (numeric && unformatted === null) unformatted = '.'
|
|
|
|
return {
|
|
formatted: isDateOrTime
|
|
? item.FORMATTED.split('.')[0]
|
|
: item.FORMATTED, //WORKAROUND SHOULD BE FIXED PROPERLY (#413): We will remove the milliseconds inside the formatted value, if decimal exists and it is date, time or datetime ddtype
|
|
unformatted: unformatted
|
|
}
|
|
})
|
|
)
|
|
|
|
const currentValues =
|
|
this.clauses.queryObj[clauseIndex].elements[ind].values.length
|
|
const moreExists = NOBS - currentValues > 0
|
|
|
|
event.loadMoreFinished(moreExists)
|
|
} else {
|
|
event.loadMoreFinished(false)
|
|
}
|
|
|
|
this.whereClauseFn()
|
|
this.setToGlobals()
|
|
this.whereClauseLoading = false
|
|
|
|
/**
|
|
* If getValues request did not finish when operator was changed to IN, we need to set values after the request is finished.
|
|
* Othervise values are not beign populated
|
|
*/
|
|
if (this.pendingINValuesSet !== null) {
|
|
const { clauseIndex, queryIndex } = this.pendingINValuesSet
|
|
|
|
this.setINValues(clauseIndex, queryIndex)
|
|
|
|
this.pendingINValuesSet = null
|
|
}
|
|
|
|
this.cdf.detectChanges()
|
|
})
|
|
.catch((err: any) => {
|
|
this.whereClauseLoading = false
|
|
})
|
|
}
|
|
} catch (error) {
|
|
this.eventService.catchResponseError('public/getcolvals', error)
|
|
}
|
|
}
|
|
|
|
public async getValues(variable: any, ind: any, clauseIndex: any) {
|
|
this.whereClauseLoading = true
|
|
|
|
let libds: string = ''
|
|
|
|
if (this.libds !== undefined) {
|
|
libds = this.libds
|
|
}
|
|
|
|
this.clearValues(ind, clauseIndex)
|
|
this.clauses.queryObj[clauseIndex].elements[ind].values = []
|
|
|
|
this.clauses.queryObj[clauseIndex].elements[ind].value = ''
|
|
this.clauses.queryObj[clauseIndex].elements[ind].operator = '='
|
|
this.clauses.queryObj[clauseIndex].elements[ind].variable = variable
|
|
|
|
try {
|
|
if (this.cols !== undefined) {
|
|
for (let index = 0; index < this.cols.length; index++) {
|
|
let element = this.cols[index]
|
|
if (element.NAME === variable) {
|
|
this.selVar.index = index
|
|
}
|
|
}
|
|
|
|
const DDTYPE = this.cols[this.selVar.index].DDTYPE
|
|
|
|
this.clauses.queryObj[clauseIndex].elements[ind].ddtype = DDTYPE
|
|
|
|
this.clauses.queryObj[clauseIndex].elements[ind].type =
|
|
this.cols[this.selVar.index].TYPE
|
|
|
|
if (this.clauses.queryObj[clauseIndex].elements[ind].type === 'num') {
|
|
this.clauses.queryObj[clauseIndex].elements[ind].operators =
|
|
this.numOperators
|
|
} else {
|
|
this.clauses.queryObj[clauseIndex].elements[ind].operators =
|
|
this.charOperators
|
|
}
|
|
|
|
let dynamicWhereClauseTable: any[] = []
|
|
|
|
if (this.dynamicWhereClause) {
|
|
dynamicWhereClauseTable = this.dynamicWhereClauseCreator(
|
|
clauseIndex,
|
|
ind
|
|
)
|
|
}
|
|
|
|
const isDateOrTime = ['DATETIME', 'TIME', 'DATE'].includes(DDTYPE)
|
|
|
|
this.sasStoreService
|
|
.getQueryValues(
|
|
variable,
|
|
libds,
|
|
dynamicWhereClauseTable,
|
|
1,
|
|
this.initial_rows
|
|
)
|
|
.then((res: PublicGetcolvalsSASResponse) => {
|
|
const values = res.vals
|
|
const { STARTROW, ROWS, NOBS } = res.meta[0]
|
|
|
|
this.clauses.queryObj[clauseIndex].elements[ind].startrow = STARTROW
|
|
this.clauses.queryObj[clauseIndex].elements[ind].rows = ROWS
|
|
this.clauses.queryObj[clauseIndex].elements[ind].nobs = NOBS
|
|
|
|
this.clauses.queryObj[clauseIndex].elements[ind].values = values
|
|
.map((item: any) => {
|
|
const numeric = res.$vals.vars.UNFORMATTED.type === 'num'
|
|
const specialMissing =
|
|
numeric && isSpecialMissing(item.UNFORMATTED)
|
|
|
|
let unformatted = specialMissing
|
|
? '.' + item.UNFORMATTED
|
|
: item.UNFORMATTED
|
|
|
|
// If it's not special missing and is numeric and null then it is normal missing `.`
|
|
if (numeric && unformatted === null) unformatted = '.'
|
|
|
|
return {
|
|
formatted: isDateOrTime
|
|
? item.FORMATTED.split('.')[0]
|
|
: item.FORMATTED, //WORKAROUND SHOULD BE FIXED PROPERLY (#413): We will remove the milliseconds inside the formatted value, if decimal exists and it is date, time or datetime ddtype
|
|
unformatted: unformatted
|
|
}
|
|
})
|
|
.slice(0, 2000)
|
|
|
|
this.whereClauseFn()
|
|
this.setToGlobals()
|
|
this.whereClauseLoading = false
|
|
|
|
/**
|
|
* If getValues request did not finish when operator was changed to IN, we need to set values after the request is finished.
|
|
* Othervise values are not beign populated
|
|
*/
|
|
if (this.pendingINValuesSet !== null) {
|
|
const { clauseIndex, queryIndex } = this.pendingINValuesSet
|
|
|
|
this.setINValues(clauseIndex, queryIndex)
|
|
|
|
this.pendingINValuesSet = null
|
|
}
|
|
|
|
this.cdf.detectChanges()
|
|
})
|
|
.catch((err: any) => {
|
|
this.whereClauseLoading = false
|
|
})
|
|
}
|
|
} catch (error) {
|
|
this.eventService.catchResponseError('public/getcolvals', error)
|
|
}
|
|
}
|
|
|
|
public getlogic(logic: any) {
|
|
this.logic = logic
|
|
this.whereClauseFn()
|
|
}
|
|
|
|
public setLogic() {
|
|
this.whereClauseFn()
|
|
this.setToGlobals()
|
|
}
|
|
|
|
public isArr(val: any) {
|
|
return (
|
|
val instanceof Array &&
|
|
val.length > 0 &&
|
|
typeof val[0].checked !== 'undefined'
|
|
)
|
|
}
|
|
|
|
public selectedLabelChange(label: string, query: any) {
|
|
query.valueVariable = label === 'Variable'
|
|
query.value = ''
|
|
this.whereClauseFn()
|
|
}
|
|
|
|
public variableInputChange(
|
|
queryVariable: any,
|
|
index: number,
|
|
clauseIndex: number,
|
|
event: any
|
|
) {
|
|
this.getValues(queryVariable, index, clauseIndex)
|
|
}
|
|
|
|
public setVariableValues(valueObj: any, ind: any, clauseIndex: any) {
|
|
// clauseIndex is current clause in group clauses, ind is current query in clause
|
|
let operator = this.clauses.queryObj[clauseIndex].elements[ind].operator
|
|
|
|
if (operator !== 'BETWEEN' && operator !== 'IN' && operator !== 'NOT IN') {
|
|
this.clauses.queryObj[clauseIndex].elements[ind].value =
|
|
valueObj.toString()
|
|
}
|
|
this.whereClauseFn()
|
|
this.setToGlobals()
|
|
}
|
|
|
|
public setVariableOperator(ind: any, operator: any, clauseIndex: any) {
|
|
let currentValue = this.clauses.queryObj[clauseIndex].elements[ind].value
|
|
if (
|
|
currentValue instanceof Array &&
|
|
operator !== 'IN' &&
|
|
operator !== 'NOT IN'
|
|
) {
|
|
if (
|
|
currentValue instanceof Array &&
|
|
currentValue.length === 2 &&
|
|
typeof currentValue[0] === 'string'
|
|
) {
|
|
currentValue = currentValue[0]
|
|
} else {
|
|
currentValue = ''
|
|
}
|
|
}
|
|
|
|
this.clearValuesOperator(ind, clauseIndex)
|
|
this.clauses.queryObj[clauseIndex].elements[ind].operator = operator
|
|
// tslint:disable-next-line:max-line-length
|
|
if (
|
|
operator === '=' ||
|
|
operator === '>' ||
|
|
operator === '<' ||
|
|
operator === '<=' ||
|
|
operator === '>=' ||
|
|
operator === 'NE'
|
|
) {
|
|
this.clauses.queryObj[clauseIndex].elements[ind].value = currentValue
|
|
}
|
|
|
|
if (operator === 'BETWEEN') {
|
|
this.clauses.queryObj[clauseIndex].elements[ind].value = []
|
|
this.clauses.queryObj[clauseIndex].elements[ind].value.push(currentValue)
|
|
this.clauses.queryObj[clauseIndex].elements[ind].value.push('')
|
|
}
|
|
|
|
if (operator === 'IN' || operator === 'NOT IN') {
|
|
this.setINValues(clauseIndex, ind)
|
|
}
|
|
|
|
this.whereClauseFn()
|
|
this.setToGlobals()
|
|
}
|
|
|
|
public setINValues(clauseIndex: number, queryIndex: number) {
|
|
const vals = []
|
|
|
|
const inValues =
|
|
this.clauses.queryObj[clauseIndex].elements[queryIndex].values
|
|
|
|
if (inValues.length < 1) {
|
|
this.pendingINValuesSet = {
|
|
clauseIndex,
|
|
queryIndex
|
|
}
|
|
}
|
|
|
|
for (let index = 0; index < inValues.length; index++) {
|
|
vals.push({ checked: false, val: inValues[index].formatted })
|
|
}
|
|
|
|
this.clauses.queryObj[clauseIndex].elements[queryIndex].value = vals
|
|
}
|
|
|
|
public clearValues(ind: any, clauseIndex: any) {
|
|
this.clauses.queryObj[clauseIndex].elements[ind].value = ''
|
|
}
|
|
|
|
public clearValuesOperator(ind: any, clauseIndex: any) {
|
|
let operator = this.clauses.queryObj[clauseIndex].elements[ind].operator
|
|
if (
|
|
operator === 'BETWEEN' ||
|
|
operator === 'IN' ||
|
|
operator === 'NOT IN' ||
|
|
operator === 'CONTAINS'
|
|
) {
|
|
this.clearValues(ind, clauseIndex)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if clause is invalid and sets bool property accordingly
|
|
* It is being used in html to mark invalid clauses red
|
|
*/
|
|
public hasInvalidCluase(clauses: any): boolean {
|
|
for (let clause of clauses) {
|
|
clause['invalidClause'] = false
|
|
|
|
if (
|
|
clause.value === '' &&
|
|
!(clause.operator === 'NE' || clause.operator === 'CONTAINS')
|
|
) {
|
|
clause['invalidClause'] = true
|
|
|
|
return true
|
|
}
|
|
|
|
if (
|
|
clause.variable === null ||
|
|
clause.operator === null ||
|
|
clause.value === null
|
|
) {
|
|
clause['invalidClause'] = true
|
|
|
|
return true
|
|
}
|
|
}
|
|
|
|
clauses.invalidClause = false
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Checks if group clause is invalid and sets bool property accordingly
|
|
* It is being used in html to mark invalid group clauses red
|
|
*/
|
|
public hasInvalidGroupCluase(): boolean {
|
|
for (let i = 0; i < this.clauses.queryObj.length; i++) {
|
|
if (this.hasInvalidCluase(this.clauses.queryObj[i].elements)) {
|
|
this.clauses.queryObj[i].invalidClause = true
|
|
|
|
return true
|
|
} else {
|
|
this.clauses.queryObj[i].invalidClause = false
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
public addClause(clauseIndex: number) {
|
|
if (this.hasInvalidCluase(this.clauses.queryObj[clauseIndex].elements)) {
|
|
this.eventService.showInfoModal(
|
|
'Error',
|
|
'Cannot add new clause, when one or more clauses are invalid or empty.'
|
|
)
|
|
return
|
|
}
|
|
|
|
this.clauseAmt.push('')
|
|
this.clauses.queryObj[clauseIndex].elements.push({
|
|
logic: null,
|
|
type: null,
|
|
variable: null,
|
|
operator: null,
|
|
value: null,
|
|
values: [],
|
|
operators: []
|
|
})
|
|
|
|
let clauseLogic = this.clauses.queryObj[clauseIndex].clauseLogic
|
|
|
|
if (typeof clauseLogic === 'undefined') {
|
|
this.clauses.queryObj[clauseIndex].clauseLogic = 'AND'
|
|
} else if (clauseLogic === 'OR') {
|
|
this.clauses.queryObj[clauseIndex].clauseLogic = 'OR'
|
|
} else {
|
|
this.clauses.queryObj[clauseIndex].clauseLogic = 'AND'
|
|
}
|
|
|
|
this.whereClauseFn()
|
|
this.setToGlobals()
|
|
}
|
|
|
|
public addGroupClause() {
|
|
if (this.hasInvalidGroupCluase()) {
|
|
this.eventService.showInfoModal(
|
|
'Error',
|
|
'Cannot add new clause, when one or more clauses are invalid or empty.'
|
|
)
|
|
return
|
|
}
|
|
|
|
this.clauses.queryObj.push({
|
|
elements: [
|
|
{
|
|
logic: null,
|
|
type: null,
|
|
variable: null,
|
|
operator: null,
|
|
value: null,
|
|
values: [],
|
|
operators: []
|
|
}
|
|
]
|
|
})
|
|
this.setToGlobals()
|
|
}
|
|
|
|
public isInHtmlCollection(
|
|
value: string,
|
|
collection: HTMLCollectionOf<HTMLOptionElement>
|
|
): boolean {
|
|
for (let i = 0; i < collection.length; i++) {
|
|
if (collection[i].value === value) return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
public removeGroupClause(clauseIndex: number) {
|
|
this.clauses.queryObj.splice(clauseIndex, 1)
|
|
this.whereClauseFn()
|
|
}
|
|
|
|
public removeClause(ind: number, clauseIndex: any) {
|
|
this.clauses.queryObj[clauseIndex].elements.splice(ind, 1)
|
|
this.whereClauseFn()
|
|
this.setToGlobals()
|
|
}
|
|
|
|
public whereClauseFn(reset: boolean = false) {
|
|
const clauses = this.helperService.deepClone(this.clauses)
|
|
|
|
if (reset) clauses.queryObj[0].elements = []
|
|
|
|
if (this.libds !== undefined) {
|
|
let result = this.sasStoreService.whereClauseCreator(
|
|
clauses,
|
|
this.groupLogic,
|
|
this.libds
|
|
)
|
|
|
|
if (!reset) {
|
|
this.whereClause = result.whereClause
|
|
this.whereString = result.string
|
|
}
|
|
}
|
|
}
|
|
|
|
async ngAfterViewInit() {
|
|
this._removeQry = this.sasStoreService.removeQuery.subscribe((remove) => {
|
|
let res = remove
|
|
if (this.clauses.queryObj.length >= 1) {
|
|
this.clauses.queryObj.length = 1
|
|
if (this.clauses.queryObj[0].elements.length >= 1) {
|
|
this.clauses.queryObj[0].elements.length = 1
|
|
this.clauses.queryObj[0].elements = []
|
|
this.clauses.queryObj[0].elements.push({
|
|
logic: null,
|
|
type: null,
|
|
variable: null,
|
|
operator: null,
|
|
value: null,
|
|
values: [],
|
|
operators: []
|
|
})
|
|
this.whereClauseFn()
|
|
}
|
|
}
|
|
})
|
|
|
|
this.columnsSub = this.sasStoreService.columns.subscribe(
|
|
(response: any) => {
|
|
let cols = response.data.cols
|
|
const objPath = getFilterObjPath(globals.rootParam, this.viewboxId)
|
|
|
|
if (globals.rootParam === 'home' || globals.rootParam === 'editor') {
|
|
this.cols = cols
|
|
let some = cols[0].NAME
|
|
this.libds = response.libds
|
|
|
|
get(globals, objPath).filter.cols = JSON.parse(JSON.stringify(cols))
|
|
}
|
|
|
|
if (globals.rootParam === 'view') {
|
|
if (get(globals, objPath).filter.cols.length < 1) {
|
|
this.cols = cols
|
|
let some = cols[0].NAME
|
|
this.libds = response.libds
|
|
|
|
get(globals, objPath).filter.cols = JSON.parse(JSON.stringify(cols))
|
|
}
|
|
}
|
|
|
|
if (!this.libds) {
|
|
this.libds = response.libds
|
|
}
|
|
}
|
|
)
|
|
|
|
this.valuesSub = this.sasStoreService.values.subscribe((res: any) => {
|
|
const objPath = getFilterObjPath(globals.rootParam, this.viewboxId)
|
|
|
|
if (get(globals, objPath).filter.vals.length < 1) {
|
|
this.vals = res.vals
|
|
|
|
get(globals, objPath).filter.vals = JSON.parse(JSON.stringify(res.vals))
|
|
}
|
|
})
|
|
}
|
|
|
|
onAutocompleteLoadingMore(
|
|
event: OnLoadingMoreEvent,
|
|
variable: any,
|
|
ind: any,
|
|
clauseIndex: any
|
|
) {
|
|
this.getValuesLoadMore(event, variable, ind, clauseIndex)
|
|
}
|
|
|
|
ngAfterContentInit(): void {
|
|
this.getFromGlobals()
|
|
|
|
setTimeout(() => {
|
|
let varInput: HTMLElement | null =
|
|
document.querySelector('#vals_var_id0_0')
|
|
|
|
if (varInput) varInput.focus()
|
|
}, 500)
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
if (this.columnsSub) {
|
|
this.columnsSub.unsubscribe()
|
|
}
|
|
|
|
if (this.valuesSub) {
|
|
this.valuesSub.unsubscribe()
|
|
}
|
|
}
|
|
}
|