feat(dq rules): notnull validation when invalid cell, will auto populate a default value
This commit is contained in:
@@ -14,6 +14,7 @@ import { HelperService } from 'src/app/services/helper.service'
|
||||
import { SasStoreService } from 'src/app/services/sas-store.service'
|
||||
import { DcValidator } from 'src/app/shared/dc-validator/dc-validator'
|
||||
import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model'
|
||||
import { isEmpty } from 'src/app/shared/dc-validator/utils/isEmpty'
|
||||
import {
|
||||
EditRecordDropdownChangeEvent,
|
||||
EditRecordInputFocusedEvent
|
||||
@@ -146,23 +147,63 @@ export class EditRecordComponent implements OnInit {
|
||||
}, 0)
|
||||
}
|
||||
|
||||
async recordInputChange(event: any, colName: string) {
|
||||
async recordInputChange(event: any, colName: string): Promise<void> {
|
||||
const colRules = this.currentRecordValidator?.getRule(colName)
|
||||
const value = event.target.value
|
||||
|
||||
this.helperService.debounceCall(300, () => {
|
||||
this.validateRecordCol(colRules, value).then((valid: boolean) => {
|
||||
const index = this.currentRecordInvalidCols.indexOf(colName)
|
||||
this.updateValidationState(colName, valid)
|
||||
|
||||
if (valid) {
|
||||
if (index > -1) this.currentRecordInvalidCols.splice(index, 1)
|
||||
} else {
|
||||
if (index < 0) this.currentRecordInvalidCols.push(colName)
|
||||
if (!valid) {
|
||||
this.tryAutoPopulateNotNull(event, colName, colRules, value)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the invalid columns list based on validation result
|
||||
*/
|
||||
private updateValidationState(colName: string, valid: boolean): void {
|
||||
const index = this.currentRecordInvalidCols.indexOf(colName)
|
||||
|
||||
if (valid && index > -1) {
|
||||
this.currentRecordInvalidCols.splice(index, 1)
|
||||
} else if (!valid && index < 0) {
|
||||
this.currentRecordInvalidCols.push(colName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-populates NOTNULL default value when the field is empty and has a default
|
||||
*/
|
||||
private tryAutoPopulateNotNull(
|
||||
event: any,
|
||||
colName: string,
|
||||
colRules: DcValidation | undefined,
|
||||
value: any
|
||||
): void {
|
||||
if (
|
||||
!isEmpty(value) ||
|
||||
!this.currentRecordValidator ||
|
||||
!this.currentRecord
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const defaultValue =
|
||||
this.currentRecordValidator.getNotNullDefaultValue(colName)
|
||||
if (defaultValue === undefined) return
|
||||
|
||||
this.currentRecord[colName] = defaultValue
|
||||
event.target.value = defaultValue
|
||||
|
||||
this.validateRecordCol(colRules, defaultValue).then((isValid: boolean) => {
|
||||
this.updateValidationState(colName, isValid)
|
||||
})
|
||||
}
|
||||
|
||||
onNextRecordClick() {
|
||||
this.onNextRecord.emit()
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import { Col } from '../shared/dc-validator/models/col.model'
|
||||
import { DcValidation } from '../shared/dc-validator/models/dc-validation.model'
|
||||
import { DQRule } from '../shared/dc-validator/models/dq-rules.model'
|
||||
import { getHotDataSchema } from '../shared/dc-validator/utils/getHotDataSchema'
|
||||
import { isEmpty } from '../shared/dc-validator/utils/isEmpty'
|
||||
import { globals } from '../_globals'
|
||||
import { UploadStaterComponent } from './components/upload-stater/upload-stater.component'
|
||||
import { DynamicExtendedCellValidation } from './models/dynamicExtendedCellValidation'
|
||||
@@ -1045,12 +1046,14 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new empty row object with proper structure
|
||||
* Creates a new empty row object with proper structure.
|
||||
* Columns with NOTNULL DQ rules are pre-populated with their RULE_VALUE.
|
||||
*/
|
||||
private createEmptyRow(): any {
|
||||
const newRow: any = {}
|
||||
this.headerColumns.forEach((col: string) => {
|
||||
newRow[col] = ''
|
||||
const defaultValue = this.dcValidator?.getNotNullDefaultValue(col)
|
||||
newRow[col] = defaultValue !== undefined ? defaultValue : ''
|
||||
})
|
||||
newRow['noLinkOption'] = true
|
||||
return newRow
|
||||
@@ -2676,13 +2679,14 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
// Note: this.headerColumns and this.columnHeader contains same data
|
||||
// need to resolve redundancy
|
||||
|
||||
// default schema
|
||||
// default schema - includes NOTNULL defaults from DQ rules
|
||||
for (let i = 0; i < this.headerColumns.length; i++) {
|
||||
const colType = this.cellValidation[i].type
|
||||
|
||||
this.hotDataSchema[this.cellValidation[i].data] = getHotDataSchema(
|
||||
colType,
|
||||
this.cellValidation[i]
|
||||
this.cellValidation[i],
|
||||
this.dcValidator?.getDqDetails()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2987,6 +2991,29 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
)
|
||||
|
||||
// Auto-populate NOTNULL default when validation fails due to empty value
|
||||
hot.addHook(
|
||||
'afterValidate',
|
||||
(isValid: boolean, value: any, row: number, prop: string | number) => {
|
||||
if (isValid || !isEmpty(value)) return
|
||||
|
||||
const colName =
|
||||
typeof prop === 'string'
|
||||
? prop
|
||||
: (hot.colToProp(prop as number) as string)
|
||||
|
||||
const defaultValue = this.dcValidator?.getNotNullDefaultValue(colName)
|
||||
if (defaultValue === undefined) return
|
||||
|
||||
// Auto-populate using setTimeout to avoid modifying during validation
|
||||
setTimeout(() => {
|
||||
if (isEmpty(hot.getDataAtRowProp(row, colName))) {
|
||||
hot.setDataAtRowProp(row, colName, defaultValue, 'autoPopulate')
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
)
|
||||
|
||||
hot.addHook('beforePaste', (data: any, cords: any) => {
|
||||
const startCol = cords[0].startCol
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from './models/dc-validation.model'
|
||||
import { DQRule, DQRuleTypes } from './models/dq-rules.model'
|
||||
import { getDqDataCols } from './utils/getDqDataCols'
|
||||
import { getNotNullDefault } from './utils/getNotNullDefault'
|
||||
import { mergeColsRules } from './utils/mergeColsRules'
|
||||
import { parseColType } from './utils/parseColType'
|
||||
import { dqValidate } from './validations/dq-validation'
|
||||
@@ -133,6 +134,19 @@ export class DcValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RULE_VALUE for a NOTNULL rule on the given column.
|
||||
* Used for auto-populating default values when cells are empty.
|
||||
* Converts to number for numeric columns.
|
||||
*
|
||||
* @param col column name
|
||||
* @returns RULE_VALUE (string or number) if NOTNULL rule exists, otherwise undefined
|
||||
*/
|
||||
getNotNullDefaultValue(col: string): string | number | undefined {
|
||||
const colRule = this.getRule(col)
|
||||
return getNotNullDefault(col, this.dqrules, colRule?.type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves dropdown source for given dc validation rule
|
||||
* The values comes from MPE_SELECTBOX table
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DQRule } from '../models/dq-rules.model'
|
||||
import { getHotDataSchema } from '../utils/getHotDataSchema'
|
||||
|
||||
describe('DC Validator - hot data schema', () => {
|
||||
@@ -8,4 +9,58 @@ describe('DC Validator - hot data schema', () => {
|
||||
).toEqual(1)
|
||||
expect(getHotDataSchema('missing')).toEqual('')
|
||||
})
|
||||
|
||||
describe('NOTNULL defaults', () => {
|
||||
const dqRules: DQRule[] = [
|
||||
{
|
||||
BASE_COL: 'TEXT_COL',
|
||||
RULE_TYPE: 'NOTNULL',
|
||||
RULE_VALUE: 'default_text',
|
||||
X: 1
|
||||
},
|
||||
{ BASE_COL: 'NUM_COL', RULE_TYPE: 'NOTNULL', RULE_VALUE: '42', X: 1 }
|
||||
]
|
||||
|
||||
it('should return NOTNULL default for text column', () => {
|
||||
expect(getHotDataSchema('text', { data: 'TEXT_COL' }, dqRules)).toEqual(
|
||||
'default_text'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return NOTNULL default as number for numeric column', () => {
|
||||
expect(getHotDataSchema('numeric', { data: 'NUM_COL' }, dqRules)).toEqual(
|
||||
42
|
||||
)
|
||||
})
|
||||
|
||||
it('should fall back to type default when no NOTNULL rule exists', () => {
|
||||
expect(
|
||||
getHotDataSchema('numeric', { data: 'OTHER_COL' }, dqRules)
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
it('should prioritize NOTNULL over autocomplete first option', () => {
|
||||
const rulesWithAutocomplete: DQRule[] = [
|
||||
{
|
||||
BASE_COL: 'SELECT_COL',
|
||||
RULE_TYPE: 'NOTNULL',
|
||||
RULE_VALUE: 'priority_value',
|
||||
X: 1
|
||||
},
|
||||
{
|
||||
BASE_COL: 'SELECT_COL',
|
||||
RULE_TYPE: 'HARDSELECT',
|
||||
RULE_VALUE: 'ignored',
|
||||
X: 1
|
||||
}
|
||||
]
|
||||
expect(
|
||||
getHotDataSchema(
|
||||
'autocomplete',
|
||||
{ data: 'SELECT_COL', source: ['first', 'second'] },
|
||||
rulesWithAutocomplete
|
||||
)
|
||||
).toEqual('priority_value')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { DQRule } from '../models/dq-rules.model'
|
||||
import { getNotNullDefault } from '../utils/getNotNullDefault'
|
||||
|
||||
describe('DC Validator - getNotNullDefault', () => {
|
||||
const dqRules: DQRule[] = [
|
||||
{
|
||||
BASE_COL: 'TEXT_COL',
|
||||
RULE_TYPE: 'NOTNULL',
|
||||
RULE_VALUE: 'default_text',
|
||||
X: 1
|
||||
},
|
||||
{ BASE_COL: 'NUM_COL', RULE_TYPE: 'NOTNULL', RULE_VALUE: '42', X: 1 },
|
||||
{ BASE_COL: 'EMPTY_RULE', RULE_TYPE: 'NOTNULL', RULE_VALUE: ' ', X: 1 },
|
||||
{
|
||||
BASE_COL: 'OTHER_COL',
|
||||
RULE_TYPE: 'HARDSELECT',
|
||||
RULE_VALUE: 'some_value',
|
||||
X: 1
|
||||
}
|
||||
]
|
||||
|
||||
it('should return string value for text columns', () => {
|
||||
expect(getNotNullDefault('TEXT_COL', dqRules, 'text')).toEqual(
|
||||
'default_text'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return number for numeric columns when RULE_VALUE is numeric', () => {
|
||||
expect(getNotNullDefault('NUM_COL', dqRules, 'numeric')).toEqual(42)
|
||||
})
|
||||
|
||||
it('should return string for numeric columns when RULE_VALUE is not numeric', () => {
|
||||
const rulesWithNonNumeric: DQRule[] = [
|
||||
{
|
||||
BASE_COL: 'NUM_COL',
|
||||
RULE_TYPE: 'NOTNULL',
|
||||
RULE_VALUE: 'not_a_number',
|
||||
X: 1
|
||||
}
|
||||
]
|
||||
expect(
|
||||
getNotNullDefault('NUM_COL', rulesWithNonNumeric, 'numeric')
|
||||
).toEqual('not_a_number')
|
||||
})
|
||||
|
||||
it('should return undefined for empty RULE_VALUE', () => {
|
||||
expect(getNotNullDefault('EMPTY_RULE', dqRules, 'text')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for columns without NOTNULL rule', () => {
|
||||
expect(getNotNullDefault('OTHER_COL', dqRules, 'text')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for non-existent columns', () => {
|
||||
expect(getNotNullDefault('MISSING_COL', dqRules, 'text')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for empty dqRules array', () => {
|
||||
expect(getNotNullDefault('TEXT_COL', [], 'text')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return string when colType is undefined', () => {
|
||||
expect(getNotNullDefault('NUM_COL', dqRules, undefined)).toEqual('42')
|
||||
})
|
||||
})
|
||||
39
client/src/app/shared/dc-validator/tests/isEmpty.spec.ts
Normal file
39
client/src/app/shared/dc-validator/tests/isEmpty.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { isEmpty } from '../utils/isEmpty'
|
||||
|
||||
describe('DC Validator - isEmpty', () => {
|
||||
it('should return true for null', () => {
|
||||
expect(isEmpty(null)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for undefined', () => {
|
||||
expect(isEmpty(undefined)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for empty string', () => {
|
||||
expect(isEmpty('')).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for whitespace-only string', () => {
|
||||
expect(isEmpty(' ')).toBe(true)
|
||||
expect(isEmpty('\t\n')).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for non-empty string', () => {
|
||||
expect(isEmpty('hello')).toBe(false)
|
||||
expect(isEmpty(' hello ')).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for number zero', () => {
|
||||
expect(isEmpty(0)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for non-zero numbers', () => {
|
||||
expect(isEmpty(42)).toBe(false)
|
||||
expect(isEmpty(-1)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for boolean values', () => {
|
||||
expect(isEmpty(true)).toBe(false)
|
||||
expect(isEmpty(false)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,6 @@
|
||||
import { DcValidation } from '../models/dc-validation.model'
|
||||
import { DQRule } from '../models/dq-rules.model'
|
||||
import { getNotNullDefault } from './getNotNullDefault'
|
||||
|
||||
const schemaTypeMap: { [key: string]: any } = {
|
||||
numeric: '',
|
||||
@@ -7,11 +9,21 @@ const schemaTypeMap: { [key: string]: any } = {
|
||||
|
||||
/**
|
||||
* Schema defines the default values for given types. For example when new row is added.
|
||||
* Priority: NOTNULL RULE_VALUE > autocomplete first option > type default
|
||||
*/
|
||||
export const getHotDataSchema = (
|
||||
export function getHotDataSchema(
|
||||
type: string | undefined,
|
||||
cellValidation?: DcValidation
|
||||
): any => {
|
||||
cellValidation?: DcValidation,
|
||||
dqRules?: DQRule[]
|
||||
): any {
|
||||
// Check for NOTNULL default first
|
||||
if (dqRules && cellValidation?.data) {
|
||||
const defaultValue = getNotNullDefault(cellValidation.data, dqRules, type)
|
||||
if (defaultValue !== undefined) {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
if (!type) return schemaTypeMap.default
|
||||
|
||||
switch (type) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { DQRule } from '../models/dq-rules.model'
|
||||
|
||||
/**
|
||||
* Returns the NOTNULL default value for a column from DQ rules.
|
||||
* Converts to number for numeric columns based on colType parameter.
|
||||
*
|
||||
* @param colName column name to look up
|
||||
* @param dqRules array of DQ rules
|
||||
* @param colType column type (e.g., 'numeric', 'text')
|
||||
* @returns default value (string or number) if NOTNULL rule exists with non-empty value, otherwise undefined
|
||||
*/
|
||||
export function getNotNullDefault(
|
||||
colName: string,
|
||||
dqRules: DQRule[],
|
||||
colType?: string
|
||||
): string | number | undefined {
|
||||
const notNullRule = dqRules.find(
|
||||
(rule: DQRule) => rule.BASE_COL === colName && rule.RULE_TYPE === 'NOTNULL'
|
||||
)
|
||||
|
||||
if (!notNullRule?.RULE_VALUE || notNullRule.RULE_VALUE.trim().length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (colType === 'numeric' && !isNaN(Number(notNullRule.RULE_VALUE))) {
|
||||
return Number(notNullRule.RULE_VALUE)
|
||||
}
|
||||
|
||||
return notNullRule.RULE_VALUE
|
||||
}
|
||||
8
client/src/app/shared/dc-validator/utils/isEmpty.ts
Normal file
8
client/src/app/shared/dc-validator/utils/isEmpty.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Checks if a value is considered empty for NOTNULL validation purposes.
|
||||
* A value is empty if it's null, undefined, or a string that is blank after trimming.
|
||||
*/
|
||||
export function isEmpty(value: unknown): boolean {
|
||||
if (value === null || value === undefined) return true
|
||||
return value.toString().trim().length === 0
|
||||
}
|
||||
Reference in New Issue
Block a user