From 96f2518af9e547956be5862a1322d9ab8e07369b Mon Sep 17 00:00:00 2001 From: M Date: Tue, 10 Feb 2026 12:29:15 +0100 Subject: [PATCH] feat(dq rules): notnull validation when invalid cell, will auto populate a default value --- .../edit-record/edit-record.component.ts | 53 +++++++++++++-- client/src/app/editor/editor.component.ts | 35 ++++++++-- .../app/shared/dc-validator/dc-validator.ts | 14 ++++ .../tests/getHotDataSchema.spec.ts | 55 ++++++++++++++++ .../tests/getNotNullDefault.spec.ts | 65 +++++++++++++++++++ .../shared/dc-validator/tests/isEmpty.spec.ts | 39 +++++++++++ .../dc-validator/utils/getHotDataSchema.ts | 18 ++++- .../dc-validator/utils/getNotNullDefault.ts | 30 +++++++++ .../app/shared/dc-validator/utils/isEmpty.ts | 8 +++ 9 files changed, 304 insertions(+), 13 deletions(-) create mode 100644 client/src/app/shared/dc-validator/tests/getNotNullDefault.spec.ts create mode 100644 client/src/app/shared/dc-validator/tests/isEmpty.spec.ts create mode 100644 client/src/app/shared/dc-validator/utils/getNotNullDefault.ts create mode 100644 client/src/app/shared/dc-validator/utils/isEmpty.ts diff --git a/client/src/app/editor/components/edit-record/edit-record.component.ts b/client/src/app/editor/components/edit-record/edit-record.component.ts index 96604f9..c81d07d 100644 --- a/client/src/app/editor/components/edit-record/edit-record.component.ts +++ b/client/src/app/editor/components/edit-record/edit-record.component.ts @@ -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 { 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() } diff --git a/client/src/app/editor/editor.component.ts b/client/src/app/editor/editor.component.ts index 155e593..8aa3bc1 100644 --- a/client/src/app/editor/editor.component.ts +++ b/client/src/app/editor/editor.component.ts @@ -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 diff --git a/client/src/app/shared/dc-validator/dc-validator.ts b/client/src/app/shared/dc-validator/dc-validator.ts index b87f541..a2beccb 100644 --- a/client/src/app/shared/dc-validator/dc-validator.ts +++ b/client/src/app/shared/dc-validator/dc-validator.ts @@ -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 diff --git a/client/src/app/shared/dc-validator/tests/getHotDataSchema.spec.ts b/client/src/app/shared/dc-validator/tests/getHotDataSchema.spec.ts index 2ba5fd5..254cf6d 100644 --- a/client/src/app/shared/dc-validator/tests/getHotDataSchema.spec.ts +++ b/client/src/app/shared/dc-validator/tests/getHotDataSchema.spec.ts @@ -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') + }) + }) }) diff --git a/client/src/app/shared/dc-validator/tests/getNotNullDefault.spec.ts b/client/src/app/shared/dc-validator/tests/getNotNullDefault.spec.ts new file mode 100644 index 0000000..4357438 --- /dev/null +++ b/client/src/app/shared/dc-validator/tests/getNotNullDefault.spec.ts @@ -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') + }) +}) diff --git a/client/src/app/shared/dc-validator/tests/isEmpty.spec.ts b/client/src/app/shared/dc-validator/tests/isEmpty.spec.ts new file mode 100644 index 0000000..166f366 --- /dev/null +++ b/client/src/app/shared/dc-validator/tests/isEmpty.spec.ts @@ -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) + }) +}) diff --git a/client/src/app/shared/dc-validator/utils/getHotDataSchema.ts b/client/src/app/shared/dc-validator/utils/getHotDataSchema.ts index 90a70e5..d47602f 100644 --- a/client/src/app/shared/dc-validator/utils/getHotDataSchema.ts +++ b/client/src/app/shared/dc-validator/utils/getHotDataSchema.ts @@ -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) { diff --git a/client/src/app/shared/dc-validator/utils/getNotNullDefault.ts b/client/src/app/shared/dc-validator/utils/getNotNullDefault.ts new file mode 100644 index 0000000..76269ad --- /dev/null +++ b/client/src/app/shared/dc-validator/utils/getNotNullDefault.ts @@ -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 +} diff --git a/client/src/app/shared/dc-validator/utils/isEmpty.ts b/client/src/app/shared/dc-validator/utils/isEmpty.ts new file mode 100644 index 0000000..5456a7e --- /dev/null +++ b/client/src/app/shared/dc-validator/utils/isEmpty.ts @@ -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 +}