fix: ensure only one REGEX applies at a time
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user