fix: ensure only one REGEX applies at a time
Build / Build-and-ng-test (pull_request) Successful in 5m32s
Build / Build-and-test-development (pull_request) Canceled after 7m12s
Lighthouse Checks / lighthouse (pull_request) Canceled after 10m14s

This commit is contained in:
4gl
2026-07-27 18:05:30 +01:00
parent 180c2477ed
commit 8fb58eb36e
7 changed files with 35 additions and 44 deletions
+3 -3
View File
@@ -30,8 +30,8 @@ flowchart TD
C -- No --> D{HARDREGEX rule on column?}
D -- Yes --> E{Pattern matches?}
E -- No --> F[Invalid: submission blocked,<br/>red htInvalid + REGEX tooltip]
E -- Yes --> G{SOFTREGEX rule on column?}
D -- No --> G
E -- Yes --> J[Valid]
D -- No --> G{SOFTREGEX rule on column?}
G -- Yes --> H{Pattern matches?}
H -- No --> I[Warning: yellow dc-warning-cell<br/>+ REGEX tooltip, submission allowed]
H -- Yes --> J[Valid]
@@ -70,7 +70,7 @@ Blank values (`undefined`, `null`, `''`) are exempt from pattern matching on any
### Precedence: HARD and SOFT on the same column
A column may carry both rules. Both the renderer and `failsSoftRegex` evaluate them independently but with hard-first precedence: HARDREGEX is checked first; a value failing it gets the red invalid styling and a tooltip, and SOFTREGEX is never evaluated for that value (the yellow warning would be redundant). If HARDREGEX passes (or is absent), SOFTREGEX is evaluated independently.
Only one regex ever runs per column. If a HARDREGEX rule exists, SOFTREGEX is ignored entirely - never compiled, never evaluated - regardless of whether individual cell values pass or fail the hard rule. A value failing HARDREGEX gets the red invalid styling (blocking submission) plus a `REGEX: <pattern>` tooltip; a SOFTREGEX-only column warns in yellow without blocking. This holds in both the renderer and `failsSoftRegex`.
### Other behaviour
+4 -4
View File
@@ -360,7 +360,7 @@ context('editor tests: ', function () {
})
})
it('12 | REGEX_BOTH_COL: passing HARDREGEX but failing SOFTREGEX warns without blocking', (done) => {
it('12 | REGEX_BOTH_COL: SOFTREGEX is ignored entirely when HARDREGEX is present', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
clickOnEdit(() => {
@@ -370,7 +370,7 @@ context('editor tests: ', function () {
scrollGridRight()
// 'AB' passes HARDREGEX (uppercase only) but fails SOFTREGEX (too
// short) - previously always inert whenever a column had both rules.
// short) - only one regex runs per column, so no warning is shown.
getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
.dblclick({ force: true })
.then(() => {
@@ -379,8 +379,8 @@ context('editor tests: ', function () {
.type('AB{enter}')
.then(() => {
getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
.should('have.class', 'dc-warning-cell')
.and('have.attr', 'title', 'REGEX: /^.{5,10}$/')
.should('not.have.class', 'dc-warning-cell')
.and('not.have.attr', 'title')
submitTable(() => {
cy.get('#submitBtn', { timeout: longerCommandTimeout })
@@ -260,7 +260,7 @@ describe('makeRegexWarningRenderer', () => {
container.remove()
})
it('falls through to SOFTREGEX when the value passes HARDREGEX but fails SOFTREGEX', () => {
it('never warns for SOFTREGEX when the column also has HARDREGEX, even for a value that passes HARDREGEX', () => {
const container = document.createElement('div')
document.body.appendChild(container)
@@ -279,10 +279,10 @@ describe('makeRegexWarningRenderer', () => {
const td = hot.getCell(0, 0)
// 'AB' passes HARDREGEX (uppercase/digits) but fails SOFTREGEX (too
// short) - this is the case that was previously always inert (no
// warning ever shown for a column with both rules).
expect(td?.classList.contains('dc-warning-cell')).toBeTrue()
expect(td?.title).toEqual('REGEX: ^.{5,10}$')
// short) - only one regex runs per column, so the soft rule is
// ignored entirely: no warning, no title.
expect(td?.classList.contains('dc-warning-cell')).toBeFalse()
expect(td?.title).toEqual('')
hot.destroy()
container.remove()
@@ -22,10 +22,12 @@ const compileRegex = (
* `REGEX: <pattern>` title, plus a yellow `dc-warning-cell` class when only
* SOFTREGEX fails, same split as makeNumberFormatRenderer.
*
* A column can carry both rules at once. Hard is validated first: a value
* failing HARDREGEX gets its title (no yellow class - red htInvalid already
* covers the color), and SOFTREGEX is only evaluated once HARDREGEX passes.
* This mirrors DcValidator.failsSoftRegex's own precedence.
* Only one regex ever runs per column: when a HARDREGEX rule exists,
* SOFTREGEX is ignored entirely (never compiled, never evaluated) -
* regardless of whether individual cell values pass or fail the hard
* rule. A value failing HARDREGEX gets its title (no yellow class - red
* htInvalid already covers the color). This mirrors
* DcValidator.failsSoftRegex's own precedence.
*
* Suppressed on rows marked for delete (_____DELETE__THIS__RECORD_____ =
* 'Yes') — a warning about data about to be removed is just noise.
@@ -38,8 +40,8 @@ export const makeRegexWarningRenderer = (
hardPattern?: string,
isNumeric: boolean = false
) => {
const softRegex = compileRegex(softPattern, 'SOFTREGEX')
const hardRegex = compileRegex(hardPattern, 'HARDREGEX')
const softRegex = hardRegex ? null : compileRegex(softPattern, 'SOFTREGEX')
const baseRenderer = Handsontable.renderers.getRenderer('text')
@@ -206,10 +206,9 @@ export class DcValidator {
/**
* Returns the RULE_VALUEs of a HARDREGEX/SOFTREGEX rule on the given
* column, for display in the column-header info dropdown. A column can
* carry both rules at once (HARDREGEX blocks submission, SOFTREGEX only
* warns), so both are surfaced independently rather than one taking
* precedence over the other.
* column, for display in the column-header info dropdown. Both are
* returned raw - the caller (buildColInfoHtml) decides which one is
* actually applied (HARDREGEX wins when both exist).
*
* @param col column name
*/
@@ -275,11 +274,11 @@ export class DcValidator {
* edit-record modal, which has no grid renderer to hook into, uses this
* directly to show the same warning outside the grid.
*
* A column can carry both HARDREGEX and SOFTREGEX at once. HARDREGEX is
* checked first: if this value fails it, SOFTREGEX is never evaluated —
* the cell is already red/blocked, so a yellow warning on top would be
* redundant. If HARDREGEX passes (or doesn't apply), SOFTREGEX is checked
* independently — same precedence as makeRegexWarningRenderer.
* A column can carry both HARDREGEX and SOFTREGEX at once, but only one
* regex ever runs per column: if a HARDREGEX rule exists, SOFTREGEX is
* ignored entirely - the cell is already governed by the blocking rule,
* so a yellow warning on top would be redundant, even for values that
* pass the hard rule. Same precedence as makeRegexWarningRenderer.
*/
failsSoftRegex(col: string, value: any): boolean {
const isNumeric =
@@ -289,16 +288,7 @@ export class DcValidator {
const hardRegexRule = this.dqrules.find(
(rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'HARDREGEX'
)
if (hardRegexRule) {
try {
if (!parseRegexRule(hardRegexRule.RULE_VALUE).test(value.toString())) {
return false
}
} catch (e) {
// Malformed HARDREGEX is treated as always-valid (see dqValidate) -
// fall through to SOFTREGEX.
}
}
if (hardRegexRule) return false
const softRegexRule = this.dqrules.find(
(rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'SOFTREGEX'
@@ -776,7 +776,7 @@ describe('DC Validator', () => {
expect(dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'ab')).toBeFalse()
})
it('is true when a value passes HARDREGEX but fails SOFTREGEX', () => {
it('is false for every value when the column has HARDREGEX - SOFTREGEX never runs, even if HARDREGEX passes', () => {
const dcValidator = buildValidator([
{
BASE_COL: 'SOME_CHAR_ANY',
@@ -793,9 +793,9 @@ describe('DC Validator', () => {
])
// 'AB' passes HARDREGEX (uppercase/digits) but fails SOFTREGEX (too
// short) -
// evaluated independently once HARDREGEX passes.
expect(dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'AB')).toBeTrue()
// short) - only one regex runs per column, so the soft rule is
// ignored entirely.
expect(dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'AB')).toBeFalse()
})
})
+3 -4
View File
@@ -16,10 +16,9 @@ export function buildColInfoHtml(
let html = `NAME: ${colName}<br>LABEL: ${colInfo.label}<br>TYPE: ${colInfo.type}<br>LENGTH: ${colInfo.length}<br>FORMAT: ${colInfo.format}`
// Only ever one REGEX rule is applied per column: when both HARDREGEX
// and SOFTREGEX exist, HARDREGEX is evaluated first and blocks
// submission, so the soft rule only applies once hard passes (same
// precedence as makeRegexWarningRenderer / DcValidator.failsSoftRegex).
// Show only the rule that is applied.
// and SOFTREGEX exist, SOFTREGEX is ignored entirely (same precedence as
// makeRegexWarningRenderer / DcValidator.failsSoftRegex). Show only the
// rule that is applied.
if (hardRegexValue) {
html += `<br>HARDREGEX: ${hardRegexValue}`
} else if (softRegexValue) {