Compare commits
5
Commits
44bc7f7fea
...
f171375899
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f171375899 | ||
|
|
57db1179a9 | ||
|
|
d13fab267f | ||
|
|
aaf406b386 | ||
|
|
39c8855f37 |
@@ -212,6 +212,12 @@ context('editor tests: ', function () {
|
|||||||
.clear()
|
.clear()
|
||||||
.type('not-an-email{enter}')
|
.type('not-an-email{enter}')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
getCellByHeaderAndRow(1, 'REGEX_HARD_COL').should(
|
||||||
|
'have.attr',
|
||||||
|
'title',
|
||||||
|
'REGEX: /[\\w.]+@[\\w]+\\.[a-z]{2,}/'
|
||||||
|
)
|
||||||
|
|
||||||
submitTable(() => {
|
submitTable(() => {
|
||||||
cy.get('.modal-body').then((modalBody: any) => {
|
cy.get('.modal-body').then((modalBody: any) => {
|
||||||
if (
|
if (
|
||||||
@@ -245,10 +251,13 @@ context('editor tests: ', function () {
|
|||||||
.clear()
|
.clear()
|
||||||
.type('not a postcode{enter}')
|
.type('not a postcode{enter}')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
getCellByHeaderAndRow(1, 'REGEX_SOFT_COL').should(
|
getCellByHeaderAndRow(1, 'REGEX_SOFT_COL')
|
||||||
'have.class',
|
.should('have.class', 'dc-warning-cell')
|
||||||
'dc-warning-cell'
|
.and(
|
||||||
)
|
'have.attr',
|
||||||
|
'title',
|
||||||
|
'REGEX: /[A-Z]{1,2}\\d{1,2}[A-Z]?\\s?\\d[A-Z]{2}/'
|
||||||
|
)
|
||||||
|
|
||||||
submitTable(() => {
|
submitTable(() => {
|
||||||
// Validation passed despite the SOFTREGEX warning: the
|
// Validation passed despite the SOFTREGEX warning: the
|
||||||
@@ -264,6 +273,124 @@ context('editor tests: ', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('9 | Info dropdown shows the applied HARDREGEX/SOFTREGEX pattern', () => {
|
||||||
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
||||||
|
|
||||||
|
clickOnEdit(() => {
|
||||||
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
||||||
|
timeout: longerCommandTimeout
|
||||||
|
}).then(() => {
|
||||||
|
scrollGridRight()
|
||||||
|
|
||||||
|
openColumnDropdown('REGEX_HARD_COL')
|
||||||
|
cy.get('.htDropdownMenu').should(($menu) => {
|
||||||
|
expect($menu.text()).to.include('REGEX: /[\\w.]+@[\\w]+\\.[a-z]{2,}/')
|
||||||
|
})
|
||||||
|
cy.get('body').click(0, 0) // close menu
|
||||||
|
|
||||||
|
openColumnDropdown('REGEX_SOFT_COL')
|
||||||
|
cy.get('.htDropdownMenu').should(($menu) => {
|
||||||
|
expect($menu.text()).to.include(
|
||||||
|
'REGEX: /[A-Z]{1,2}\\d{1,2}[A-Z]?\\s?\\d[A-Z]{2}/'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('10 | Info dropdown labels HARDREGEX/SOFTREGEX separately when a column has both', () => {
|
||||||
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
||||||
|
|
||||||
|
clickOnEdit(() => {
|
||||||
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
||||||
|
timeout: longerCommandTimeout
|
||||||
|
}).then(() => {
|
||||||
|
scrollGridRight()
|
||||||
|
|
||||||
|
openColumnDropdown('REGEX_BOTH_COL')
|
||||||
|
cy.get('.htDropdownMenu').should(($menu) => {
|
||||||
|
const text = $menu.text()
|
||||||
|
|
||||||
|
expect(text).to.include('HARDREGEX: /^[A-Z0-9_-]+$/')
|
||||||
|
expect(text).to.include('SOFTREGEX: /^.{5,10}$/')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('11 | REGEX_BOTH_COL: a HARDREGEX failure blocks submission and sets its own tooltip, not yellow', (done) => {
|
||||||
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
||||||
|
|
||||||
|
clickOnEdit(() => {
|
||||||
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
||||||
|
timeout: longerCommandTimeout
|
||||||
|
}).then(() => {
|
||||||
|
scrollGridRight()
|
||||||
|
|
||||||
|
// 'bad value' fails HARDREGEX (lowercase + space) but is 9 chars,
|
||||||
|
// within SOFTREGEX's 5-10 range - isolates the hard-only failure.
|
||||||
|
getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
|
||||||
|
.dblclick({ force: true })
|
||||||
|
.then(() => {
|
||||||
|
cy.focused()
|
||||||
|
.clear()
|
||||||
|
.type('bad value{enter}')
|
||||||
|
.then(() => {
|
||||||
|
getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
|
||||||
|
.should('not.have.class', 'dc-warning-cell')
|
||||||
|
.and('have.attr', 'title', 'REGEX: /^[A-Z0-9_-]+$/')
|
||||||
|
|
||||||
|
submitTable(() => {
|
||||||
|
cy.get('.modal-body').then((modalBody: any) => {
|
||||||
|
if (
|
||||||
|
modalBody[0].innerHTML
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(`invalid values are present`)
|
||||||
|
) {
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('12 | REGEX_BOTH_COL: passing HARDREGEX but failing SOFTREGEX warns without blocking', (done) => {
|
||||||
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
||||||
|
|
||||||
|
clickOnEdit(() => {
|
||||||
|
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
||||||
|
timeout: longerCommandTimeout
|
||||||
|
}).then(() => {
|
||||||
|
scrollGridRight()
|
||||||
|
|
||||||
|
// 'AB' passes HARDREGEX (uppercase only) but fails SOFTREGEX (too
|
||||||
|
// short) - previously always inert whenever a column had both rules.
|
||||||
|
getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
|
||||||
|
.dblclick({ force: true })
|
||||||
|
.then(() => {
|
||||||
|
cy.focused()
|
||||||
|
.clear()
|
||||||
|
.type('AB{enter}')
|
||||||
|
.then(() => {
|
||||||
|
getCellByHeaderAndRow(1, 'REGEX_BOTH_COL')
|
||||||
|
.should('have.class', 'dc-warning-cell')
|
||||||
|
.and('have.attr', 'title', 'REGEX: /^.{5,10}$/')
|
||||||
|
|
||||||
|
submitTable(() => {
|
||||||
|
cy.get('#submitBtn', { timeout: longerCommandTimeout })
|
||||||
|
.should('exist')
|
||||||
|
.should('not.be.disabled')
|
||||||
|
.then(() => done())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handsontable virtualizes columns — with 17 columns on MPE_X_NEW, only
|
// Handsontable virtualizes columns — with 17 columns on MPE_X_NEW, only
|
||||||
@@ -280,6 +407,24 @@ const scrollGridRight = () => {
|
|||||||
.scrollTo('right')
|
.scrollTo('right')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Opens a column header's dropdown menu to reach its `info` item, which
|
||||||
|
// has a custom renderer showing NAME/LABEL/TYPE/LENGTH/FORMAT and, when the
|
||||||
|
// column has a HARDREGEX/SOFTREGEX rule, the applied pattern. Clicking the
|
||||||
|
// header text first selects the column - the renderer reads
|
||||||
|
// hot.getSelected() to decide which column to describe.
|
||||||
|
const openColumnDropdown = (headerText: string) => {
|
||||||
|
cy.get('#hotTable .ht_clone_top .htCore thead button.changeType', {
|
||||||
|
timeout: longerCommandTimeout
|
||||||
|
})
|
||||||
|
.parents('th')
|
||||||
|
.filter((_, th) => Cypress.$(th).text().includes(headerText))
|
||||||
|
.last()
|
||||||
|
.as('targetHeader')
|
||||||
|
|
||||||
|
cy.get('@targetHeader').click()
|
||||||
|
cy.get('@targetHeader').find('button.changeType').click({ force: true })
|
||||||
|
}
|
||||||
|
|
||||||
// Locates a body cell by its column's header text rather than a hardcoded
|
// Locates a body cell by its column's header text rather than a hardcoded
|
||||||
// childNodes index. Handsontable's hiddenColumns plugin (e.g. HIDDEN_COL,
|
// childNodes index. Handsontable's hiddenColumns plugin (e.g. HIDDEN_COL,
|
||||||
// used by several demo columns ahead of REGEX_HARD_COL/REGEX_SOFT_COL in
|
// used by several demo columns ahead of REGEX_HARD_COL/REGEX_SOFT_COL in
|
||||||
|
|||||||
@@ -3254,7 +3254,15 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
colName = this.hotInstance?.colToProp(selectedCol) as string
|
colName = this.hotInstance?.colToProp(selectedCol) as string
|
||||||
colInfo = this.$dataFormats?.vars[colName]
|
colInfo = this.$dataFormats?.vars[colName]
|
||||||
|
|
||||||
textInfo = buildColInfoHtml(colName, colInfo)
|
const { hardRegexValue, softRegexValue } =
|
||||||
|
this.dcValidator?.getRegexRuleValues(colName) || {}
|
||||||
|
|
||||||
|
textInfo = buildColInfoHtml(
|
||||||
|
colName,
|
||||||
|
colInfo,
|
||||||
|
hardRegexValue,
|
||||||
|
softRegexValue
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
elem.innerHTML = textInfo
|
elem.innerHTML = textInfo
|
||||||
|
|||||||
@@ -19,13 +19,14 @@ describe('makeRegexWarningRenderer', () => {
|
|||||||
return { hot, container }
|
return { hot, container }
|
||||||
}
|
}
|
||||||
|
|
||||||
it('adds dc-warning-cell when the value fails the pattern', () => {
|
it('adds dc-warning-cell and a REGEX: title when the value fails the pattern', () => {
|
||||||
const { hot, container } = buildHot([
|
const { hot, container } = buildHot([
|
||||||
{ val: 'abc', _____DELETE__THIS__RECORD_____: 'No' }
|
{ val: 'abc', _____DELETE__THIS__RECORD_____: 'No' }
|
||||||
])
|
])
|
||||||
|
|
||||||
const td = hot.getCell(0, 0)
|
const td = hot.getCell(0, 0)
|
||||||
expect(td?.classList.contains('dc-warning-cell')).toBeTrue()
|
expect(td?.classList.contains('dc-warning-cell')).toBeTrue()
|
||||||
|
expect(td?.title).toEqual('REGEX: ^[A-Z]{3}$')
|
||||||
|
|
||||||
hot.destroy()
|
hot.destroy()
|
||||||
container.remove()
|
container.remove()
|
||||||
@@ -128,4 +129,154 @@ describe('makeRegexWarningRenderer', () => {
|
|||||||
hot.destroy()
|
hot.destroy()
|
||||||
container.remove()
|
container.remove()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('HARDREGEX (second, optional pattern)', () => {
|
||||||
|
// HARDREGEX already blocks submission via dqValidate/HOT's own
|
||||||
|
// htInvalid (unchanged, untouched here) - this renderer only adds the
|
||||||
|
// matching 'REGEX: <pattern>' title on top, so a column with only a
|
||||||
|
// HARDREGEX rule (no SOFTREGEX at all) still tells the user why a cell
|
||||||
|
// is red, not just that it is.
|
||||||
|
const buildHardOnlyHot = (value: string) => {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
document.body.appendChild(container)
|
||||||
|
|
||||||
|
const hot = new Handsontable(container, {
|
||||||
|
data: [{ val: value, _____DELETE__THIS__RECORD_____: 'No' }],
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
data: 'val',
|
||||||
|
renderer: makeRegexWarningRenderer(undefined, '^[A-Z]{3}$')
|
||||||
|
},
|
||||||
|
{ data: '_____DELETE__THIS__RECORD_____' }
|
||||||
|
],
|
||||||
|
licenseKey: 'non-commercial-and-evaluation'
|
||||||
|
})
|
||||||
|
hot.render()
|
||||||
|
|
||||||
|
return { hot, container }
|
||||||
|
}
|
||||||
|
|
||||||
|
it('sets a REGEX: title (no dc-warning-cell) when only HARDREGEX fails', () => {
|
||||||
|
const { hot, container } = buildHardOnlyHot('abc')
|
||||||
|
|
||||||
|
const td = hot.getCell(0, 0)
|
||||||
|
expect(td?.classList.contains('dc-warning-cell')).toBeFalse()
|
||||||
|
expect(td?.title).toEqual('REGEX: ^[A-Z]{3}$')
|
||||||
|
|
||||||
|
hot.destroy()
|
||||||
|
container.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets no title when the value passes HARDREGEX', () => {
|
||||||
|
const { hot, container } = buildHardOnlyHot('ABC')
|
||||||
|
|
||||||
|
const td = hot.getCell(0, 0)
|
||||||
|
expect(td?.title).toEqual('')
|
||||||
|
|
||||||
|
hot.destroy()
|
||||||
|
container.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('suppresses the HARDREGEX title on a row marked for delete', () => {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
document.body.appendChild(container)
|
||||||
|
|
||||||
|
const hot = new Handsontable(container, {
|
||||||
|
data: [{ val: 'abc', _____DELETE__THIS__RECORD_____: 'Yes' }],
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
data: 'val',
|
||||||
|
renderer: makeRegexWarningRenderer(undefined, '^[A-Z]{3}$')
|
||||||
|
},
|
||||||
|
{ data: '_____DELETE__THIS__RECORD_____' }
|
||||||
|
],
|
||||||
|
licenseKey: 'non-commercial-and-evaluation'
|
||||||
|
})
|
||||||
|
hot.render()
|
||||||
|
|
||||||
|
expect(hot.getCell(0, 0)?.title).toEqual('')
|
||||||
|
|
||||||
|
hot.destroy()
|
||||||
|
container.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prefers the HARDREGEX title over SOFTREGEX when a value fails both', () => {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
document.body.appendChild(container)
|
||||||
|
|
||||||
|
const hot = new Handsontable(container, {
|
||||||
|
data: [{ val: 'ab', _____DELETE__THIS__RECORD_____: 'No' }],
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
data: 'val',
|
||||||
|
renderer: makeRegexWarningRenderer('^.{5,10}$', '^[A-Z]{3}$')
|
||||||
|
},
|
||||||
|
{ data: '_____DELETE__THIS__RECORD_____' }
|
||||||
|
],
|
||||||
|
licenseKey: 'non-commercial-and-evaluation'
|
||||||
|
})
|
||||||
|
hot.render()
|
||||||
|
|
||||||
|
const td = hot.getCell(0, 0)
|
||||||
|
// 'ab' fails HARDREGEX (not 3 uppercase letters) AND SOFTREGEX (too
|
||||||
|
// short) - HARDREGEX wins: its title shows, and no yellow class is
|
||||||
|
// added (HOT's own red htInvalid governs this cell instead).
|
||||||
|
expect(td?.classList.contains('dc-warning-cell')).toBeFalse()
|
||||||
|
expect(td?.title).toEqual('REGEX: ^[A-Z]{3}$')
|
||||||
|
|
||||||
|
hot.destroy()
|
||||||
|
container.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls through to SOFTREGEX when the value passes HARDREGEX but fails SOFTREGEX', () => {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
document.body.appendChild(container)
|
||||||
|
|
||||||
|
const hot = new Handsontable(container, {
|
||||||
|
data: [{ val: 'AB', _____DELETE__THIS__RECORD_____: 'No' }],
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
data: 'val',
|
||||||
|
renderer: makeRegexWarningRenderer('^.{5,10}$', '^[A-Z0-9]+$')
|
||||||
|
},
|
||||||
|
{ data: '_____DELETE__THIS__RECORD_____' }
|
||||||
|
],
|
||||||
|
licenseKey: 'non-commercial-and-evaluation'
|
||||||
|
})
|
||||||
|
hot.render()
|
||||||
|
|
||||||
|
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}$')
|
||||||
|
|
||||||
|
hot.destroy()
|
||||||
|
container.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not throw and never warns on a malformed HARDREGEX pattern', () => {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
document.body.appendChild(container)
|
||||||
|
|
||||||
|
const hot = new Handsontable(container, {
|
||||||
|
data: [{ val: 'abc', _____DELETE__THIS__RECORD_____: 'No' }],
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
data: 'val',
|
||||||
|
renderer: makeRegexWarningRenderer(undefined, '[unterminated')
|
||||||
|
},
|
||||||
|
{ data: '_____DELETE__THIS__RECORD_____' }
|
||||||
|
],
|
||||||
|
licenseKey: 'non-commercial-and-evaluation'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(() => hot.render()).not.toThrow()
|
||||||
|
expect(hot.getCell(0, 0)?.title).toEqual('')
|
||||||
|
|
||||||
|
hot.destroy()
|
||||||
|
container.remove()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,28 +2,43 @@ import Handsontable from 'handsontable'
|
|||||||
import { isRegexRuleExempt } from '../../shared/dc-validator/utils/isRegexRuleExempt'
|
import { isRegexRuleExempt } from '../../shared/dc-validator/utils/isRegexRuleExempt'
|
||||||
import { parseRegexRule } from '../../shared/dc-validator/utils/parseRegexRule'
|
import { parseRegexRule } from '../../shared/dc-validator/utils/parseRegexRule'
|
||||||
|
|
||||||
|
const compileRegex = (
|
||||||
|
pattern: string | undefined,
|
||||||
|
ruleType: 'SOFTREGEX' | 'HARDREGEX'
|
||||||
|
): RegExp | null => {
|
||||||
|
try {
|
||||||
|
return pattern ? parseRegexRule(pattern) : null
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`${ruleType} - invalid pattern, warning disabled: ${pattern}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a display-only HOT renderer for SOFTREGEX: a value that fails the
|
* Builds a display-only HOT renderer for HARDREGEX/SOFTREGEX: neither ever
|
||||||
* pattern gets a yellow `dc-warning-cell` class (styles.scss) instead of
|
* returns false from the cell validator here (that's dqValidate's job for
|
||||||
* blocking submission — SOFTREGEX must never return false from the cell
|
* HARDREGEX, which blocks submission and paints HOT's own red htInvalid
|
||||||
* validator (that would paint htInvalid and block submit), so the warning
|
* independently of this renderer) - this renderer only adds the matching
|
||||||
* lives purely at the render layer, same split as makeNumberFormatRenderer.
|
* `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.
|
||||||
*
|
*
|
||||||
* Suppressed on rows marked for delete (_____DELETE__THIS__RECORD_____ =
|
* Suppressed on rows marked for delete (_____DELETE__THIS__RECORD_____ =
|
||||||
* 'Yes') — a warning about data about to be removed is just noise.
|
* 'Yes') — a warning about data about to be removed is just noise.
|
||||||
*
|
*
|
||||||
* Falls back to no warning ever showing when the pattern itself is
|
* Falls back to no warning ever showing when a pattern itself is malformed,
|
||||||
* malformed, rather than breaking the cell.
|
* rather than breaking the cell.
|
||||||
*/
|
*/
|
||||||
export const makeRegexWarningRenderer = (pattern?: string) => {
|
export const makeRegexWarningRenderer = (
|
||||||
let regex: RegExp | null = null
|
softPattern?: string,
|
||||||
|
hardPattern?: string
|
||||||
try {
|
) => {
|
||||||
if (pattern) regex = parseRegexRule(pattern)
|
const softRegex = compileRegex(softPattern, 'SOFTREGEX')
|
||||||
} catch (e) {
|
const hardRegex = compileRegex(hardPattern, 'HARDREGEX')
|
||||||
console.warn(`SOFTREGEX - invalid pattern, warning disabled: ${pattern}`)
|
|
||||||
regex = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseRenderer = Handsontable.renderers.getRenderer('text')
|
const baseRenderer = Handsontable.renderers.getRenderer('text')
|
||||||
|
|
||||||
@@ -40,13 +55,20 @@ export const makeRegexWarningRenderer = (pattern?: string) => {
|
|||||||
|
|
||||||
const markedForDelete =
|
const markedForDelete =
|
||||||
instance.getDataAtRowProp(row, '_____DELETE__THIS__RECORD_____') === 'Yes'
|
instance.getDataAtRowProp(row, '_____DELETE__THIS__RECORD_____') === 'Yes'
|
||||||
|
const exempt = isRegexRuleExempt(value)
|
||||||
|
|
||||||
const failsPattern =
|
const failsHard =
|
||||||
!!regex && !isRegexRuleExempt(value) && !regex.test(value.toString())
|
!!hardRegex && !exempt && !hardRegex.test(value.toString())
|
||||||
|
const failsSoft =
|
||||||
|
!!softRegex && !exempt && !softRegex.test(value.toString())
|
||||||
|
|
||||||
if (failsPattern && !markedForDelete) {
|
if (markedForDelete) return td
|
||||||
|
|
||||||
|
if (failsHard) {
|
||||||
|
td.title = `REGEX: ${hardPattern}`
|
||||||
|
} else if (failsSoft) {
|
||||||
td.classList.add('dc-warning-cell')
|
td.classList.add('dc-warning-cell')
|
||||||
td.title = 'Value does not match the expected pattern'
|
td.title = `REGEX: ${softPattern}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return td
|
return td
|
||||||
|
|||||||
@@ -204,6 +204,32 @@ export class DcValidator {
|
|||||||
return isNaN(digits) ? undefined : digits
|
return isNaN(digits) ? undefined : digits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param col column name
|
||||||
|
*/
|
||||||
|
getRegexRuleValues(col: string): {
|
||||||
|
hardRegexValue: string | undefined
|
||||||
|
softRegexValue: string | undefined
|
||||||
|
} {
|
||||||
|
const hardRegexRule = this.dqrules.find(
|
||||||
|
(rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'HARDREGEX'
|
||||||
|
)
|
||||||
|
const softRegexRule = this.dqrules.find(
|
||||||
|
(rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'SOFTREGEX'
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
hardRegexValue: hardRegexRule?.RULE_VALUE,
|
||||||
|
softRegexValue: softRegexRule?.RULE_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves dropdown source for given dc validation rule
|
* Retrieves dropdown source for given dc validation rule
|
||||||
* The values comes from MPE_SELECTBOX table
|
* The values comes from MPE_SELECTBOX table
|
||||||
@@ -247,15 +273,31 @@ export class DcValidator {
|
|||||||
* exists. SOFTREGEX never goes through dqValidate/the cell validator (see
|
* exists. SOFTREGEX never goes through dqValidate/the cell validator (see
|
||||||
* setupValidations — it's a display-only grid renderer instead), so the
|
* setupValidations — it's a display-only grid renderer instead), so the
|
||||||
* edit-record modal, which has no grid renderer to hook into, uses this
|
* edit-record modal, which has no grid renderer to hook into, uses this
|
||||||
* directly to show the same warning outside the grid. HARDREGEX takes
|
* directly to show the same warning outside the grid.
|
||||||
* precedence when both rules apply to the column, matching the grid's own
|
*
|
||||||
* behaviour (setupValidations skips wiring the SOFTREGEX renderer there
|
* A column can carry both HARDREGEX and SOFTREGEX at once. HARDREGEX is
|
||||||
* too) — a dual-rule column should render red/blocked, never yellow.
|
* 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.
|
||||||
*/
|
*/
|
||||||
failsSoftRegex(col: string, value: any): boolean {
|
failsSoftRegex(col: string, value: any): boolean {
|
||||||
if (this.hasDqRules(col, ['HARDREGEX'])) return false
|
|
||||||
if (isRegexRuleExempt(value)) return false
|
if (isRegexRuleExempt(value)) return false
|
||||||
|
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const softRegexRule = this.dqrules.find(
|
const softRegexRule = this.dqrules.find(
|
||||||
(rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'SOFTREGEX'
|
(rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'SOFTREGEX'
|
||||||
)
|
)
|
||||||
@@ -425,25 +467,20 @@ export class DcValidator {
|
|||||||
this.rules[i].numericFormat = undefined
|
this.rules[i].numericFormat = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// SOFTREGEX: display-only warning (yellow), never blocks submission —
|
// HARDREGEX/SOFTREGEX: submission-blocking for HARDREGEX still goes
|
||||||
// unlike HARDREGEX (see dq-validation.ts), which goes through the
|
// through the normal validator/dqValidate path (see dq-validation.ts)
|
||||||
// normal validator/dqValidate path instead. Last-wins against
|
// and is unaffected by this renderer. This only wires the display
|
||||||
// NUMBER_FORMAT if a column somehow carried both (not expected in
|
// layer - a 'REGEX: <pattern>' title, plus a yellow dc-warning-cell
|
||||||
// practice — one formats numbers, the other pattern-matches text).
|
// when only SOFTREGEX fails (never for HARDREGEX, which relies on
|
||||||
//
|
// HOT's own red htInvalid instead). Last-wins against NUMBER_FORMAT
|
||||||
// HARDREGEX takes precedence when both apply to the same column: skip
|
// if a column somehow carried both (not expected in practice — one
|
||||||
// wiring this renderer entirely, so a failing value stays HOT's own
|
// formats numbers, the other pattern-matches text).
|
||||||
// red htInvalid (from HARDREGEX/dqValidate) rather than being
|
if (this.hasDqRules(ruleColName, ['HARDREGEX', 'SOFTREGEX'])) {
|
||||||
// visually overridden by this renderer's yellow dc-warning-cell.
|
const { hardRegexValue, softRegexValue } =
|
||||||
if (
|
this.getRegexRuleValues(ruleColName)
|
||||||
this.hasDqRules(ruleColName, ['SOFTREGEX']) &&
|
|
||||||
!this.hasDqRules(ruleColName, ['HARDREGEX'])
|
|
||||||
) {
|
|
||||||
const softRegexRule = this.getDqDetails(ruleColName).find(
|
|
||||||
(rule: DQRule) => rule.RULE_TYPE === 'SOFTREGEX'
|
|
||||||
)
|
|
||||||
this.rules[i].renderer = makeRegexWarningRenderer(
|
this.rules[i].renderer = makeRegexWarningRenderer(
|
||||||
softRegexRule?.RULE_VALUE
|
softRegexValue,
|
||||||
|
hardRegexValue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -555,7 +555,32 @@ describe('DC Validator', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('11 | wires a function renderer for a SOFTREGEX rule, without blocking submission', () => {
|
it('11 | wires a function renderer for a HARDREGEX-only rule too (for the REGEX: tooltip)', () => {
|
||||||
|
// HARDREGEX blocking itself is covered by test 10 above - this isolates
|
||||||
|
// the newer addition: even with no SOFTREGEX at all, a renderer must
|
||||||
|
// still be wired so a failing cell gets a 'REGEX: <pattern>' title on
|
||||||
|
// top of HOT's own red htInvalid, not just silence.
|
||||||
|
const dcValidator: DcValidator = new DcValidator(
|
||||||
|
example_sasparams,
|
||||||
|
example_dataformats,
|
||||||
|
example_cols,
|
||||||
|
[
|
||||||
|
...example_dqRules,
|
||||||
|
{
|
||||||
|
BASE_COL: 'SOME_CHAR_ANY',
|
||||||
|
RULE_TYPE: 'HARDREGEX',
|
||||||
|
RULE_VALUE: '^[A-Z]+$',
|
||||||
|
X: 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
example_dqData
|
||||||
|
)
|
||||||
|
const someCharAnyRule = dcValidator.getRule('SOME_CHAR_ANY')
|
||||||
|
|
||||||
|
expect(typeof someCharAnyRule?.renderer).toEqual('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('12 | wires a function renderer for a SOFTREGEX rule, without blocking submission', () => {
|
||||||
// SOME_CHAR_ANY carries no other DQ rules in the shared fixture, so this
|
// SOME_CHAR_ANY carries no other DQ rules in the shared fixture, so this
|
||||||
// isolates SOFTREGEX's own wiring. The renderer's own pass/fail/delete-
|
// isolates SOFTREGEX's own wiring. The renderer's own pass/fail/delete-
|
||||||
// suppression behaviour is covered by regex-warning-renderer.spec.ts —
|
// suppression behaviour is covered by regex-warning-renderer.spec.ts —
|
||||||
@@ -589,11 +614,11 @@ describe('DC Validator', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('12 | HARDREGEX takes precedence over SOFTREGEX on a dual-rule column', () => {
|
it('13 | wires a renderer for a dual-rule column, and HARDREGEX still blocks submission', () => {
|
||||||
// Both rules on the same column: HARDREGEX must win, so the cell
|
// Both rules on the same column: submission blocking is still governed
|
||||||
// renders red (blocked) via HOT's own htInvalid, not yellow. No
|
// entirely by HARDREGEX/dqValidate (unchanged). A renderer is wired so
|
||||||
// SOFTREGEX renderer is wired at all here, so there's nothing that could
|
// the cell gets a 'REGEX: <pattern>' title - its own hard-vs-soft
|
||||||
// visually compete with htInvalid for that column.
|
// precedence and coloring are covered by regex-warning-renderer.spec.ts.
|
||||||
const dcValidator: DcValidator = new DcValidator(
|
const dcValidator: DcValidator = new DcValidator(
|
||||||
example_sasparams,
|
example_sasparams,
|
||||||
example_dataformats,
|
example_dataformats,
|
||||||
@@ -624,10 +649,10 @@ describe('DC Validator', () => {
|
|||||||
expect(valid).toBeFalse()
|
expect(valid).toBeFalse()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
expect(rule?.renderer).toBeUndefined()
|
expect(typeof rule?.renderer).toEqual('function')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('13 | failsSoftRegex (edit-record modal support for SOFTREGEX)', () => {
|
describe('14 | failsSoftRegex (edit-record modal support for SOFTREGEX)', () => {
|
||||||
// SOFTREGEX never goes through dqValidate (see the wiring in
|
// SOFTREGEX never goes through dqValidate (see the wiring in
|
||||||
// setupValidations), so the edit-record modal — which has no grid
|
// setupValidations), so the edit-record modal — which has no grid
|
||||||
// renderer to hook into — calls this directly to show the same warning.
|
// renderer to hook into — calls this directly to show the same warning.
|
||||||
@@ -713,25 +738,122 @@ describe('DC Validator', () => {
|
|||||||
expect(dcValidator.failsSoftRegex('SOME_CHAR_ANY', '.a')).toBeFalse()
|
expect(dcValidator.failsSoftRegex('SOME_CHAR_ANY', '.a')).toBeFalse()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('is false when the column also has HARDREGEX (precedence — no yellow on a red cell)', () => {
|
it('is false when a value fails both HARDREGEX and SOFTREGEX (precedence — no yellow on a red cell)', () => {
|
||||||
|
const dcValidator = buildValidator([
|
||||||
|
{
|
||||||
|
BASE_COL: 'SOME_CHAR_ANY',
|
||||||
|
RULE_TYPE: 'HARDREGEX',
|
||||||
|
RULE_VALUE: '^[A-Z]{3}$',
|
||||||
|
X: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BASE_COL: 'SOME_CHAR_ANY',
|
||||||
|
RULE_TYPE: 'SOFTREGEX',
|
||||||
|
RULE_VALUE: '^.{5,10}$',
|
||||||
|
X: 0
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 'ab' fails both HARDREGEX (not 3 uppercase letters) and SOFTREGEX
|
||||||
|
// (too short) - HARDREGEX wins, so this must stay false rather than
|
||||||
|
// report a SOFTREGEX warning on a value that's already blocked.
|
||||||
|
expect(dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'ab')).toBeFalse()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is true when a value passes HARDREGEX but fails SOFTREGEX', () => {
|
||||||
|
const dcValidator = buildValidator([
|
||||||
|
{
|
||||||
|
BASE_COL: 'SOME_CHAR_ANY',
|
||||||
|
RULE_TYPE: 'HARDREGEX',
|
||||||
|
RULE_VALUE: '^[A-Z0-9]+$',
|
||||||
|
X: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BASE_COL: 'SOME_CHAR_ANY',
|
||||||
|
RULE_TYPE: 'SOFTREGEX',
|
||||||
|
RULE_VALUE: '^.{5,10}$',
|
||||||
|
X: 0
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 'AB' passes HARDREGEX (uppercase/digits) but fails SOFTREGEX (too
|
||||||
|
// short) -
|
||||||
|
// evaluated independently once HARDREGEX passes.
|
||||||
|
expect(dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'AB')).toBeTrue()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('15 | getRegexRuleValues (column-header info display)', () => {
|
||||||
|
const buildValidator = (dqRules: DQRule[]) =>
|
||||||
|
new DcValidator(
|
||||||
|
example_sasparams,
|
||||||
|
example_dataformats,
|
||||||
|
example_cols,
|
||||||
|
dqRules,
|
||||||
|
example_dqData
|
||||||
|
)
|
||||||
|
|
||||||
|
it('returns only hardRegexValue for a column with just HARDREGEX', () => {
|
||||||
const dcValidator = buildValidator([
|
const dcValidator = buildValidator([
|
||||||
{
|
{
|
||||||
BASE_COL: 'SOME_CHAR_ANY',
|
BASE_COL: 'SOME_CHAR_ANY',
|
||||||
RULE_TYPE: 'HARDREGEX',
|
RULE_TYPE: 'HARDREGEX',
|
||||||
RULE_VALUE: '^[A-Z]+$',
|
RULE_VALUE: '^[A-Z]+$',
|
||||||
X: 0
|
X: 0
|
||||||
},
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(dcValidator.getRegexRuleValues('SOME_CHAR_ANY')).toEqual({
|
||||||
|
hardRegexValue: '^[A-Z]+$',
|
||||||
|
softRegexValue: undefined
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns only softRegexValue for a column with just SOFTREGEX', () => {
|
||||||
|
const dcValidator = buildValidator([
|
||||||
{
|
{
|
||||||
BASE_COL: 'SOME_CHAR_ANY',
|
BASE_COL: 'SOME_CHAR_ANY',
|
||||||
RULE_TYPE: 'SOFTREGEX',
|
RULE_TYPE: 'SOFTREGEX',
|
||||||
RULE_VALUE: '^[A-Z]+$',
|
RULE_VALUE: '/\\b(the|data)\\b/i',
|
||||||
X: 0
|
X: 0
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(
|
expect(dcValidator.getRegexRuleValues('SOME_CHAR_ANY')).toEqual({
|
||||||
dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'lowercase')
|
hardRegexValue: undefined,
|
||||||
).toBeFalse()
|
softRegexValue: '/\\b(the|data)\\b/i'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns both values for a column with both HARDREGEX and SOFTREGEX', () => {
|
||||||
|
const dcValidator = buildValidator([
|
||||||
|
{
|
||||||
|
BASE_COL: 'SOME_CHAR_ANY',
|
||||||
|
RULE_TYPE: 'HARDREGEX',
|
||||||
|
RULE_VALUE: '^HARD$',
|
||||||
|
X: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BASE_COL: 'SOME_CHAR_ANY',
|
||||||
|
RULE_TYPE: 'SOFTREGEX',
|
||||||
|
RULE_VALUE: '^SOFT$',
|
||||||
|
X: 0
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(dcValidator.getRegexRuleValues('SOME_CHAR_ANY')).toEqual({
|
||||||
|
hardRegexValue: '^HARD$',
|
||||||
|
softRegexValue: '^SOFT$'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns both undefined for a column with no regex rule', () => {
|
||||||
|
const dcValidator = buildValidator([])
|
||||||
|
|
||||||
|
expect(dcValidator.getRegexRuleValues('SOME_CHAR_ANY')).toEqual({
|
||||||
|
hardRegexValue: undefined,
|
||||||
|
softRegexValue: undefined
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,4 +18,60 @@ describe('buildColInfoHtml', () => {
|
|||||||
'NAME: SOME_CHAR<br>LABEL: Some Character Column<br>TYPE: char<br>LENGTH: 1024<br>FORMAT: $1024.'
|
'NAME: SOME_CHAR<br>LABEL: Some Character Column<br>TYPE: char<br>LENGTH: 1024<br>FORMAT: $1024.'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('appends a REGEX line when only a HARDREGEX value is provided', () => {
|
||||||
|
const colInfo: DataFormat = {
|
||||||
|
label: 'Some Character Column',
|
||||||
|
type: 'char',
|
||||||
|
length: '1024',
|
||||||
|
format: '$1024.'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildColInfoHtml('SOME_CHAR', colInfo, '/^[A-Z]+$/i', undefined)
|
||||||
|
).toBe(
|
||||||
|
'NAME: SOME_CHAR<br>LABEL: Some Character Column<br>TYPE: char<br>LENGTH: 1024<br>FORMAT: $1024.<br>REGEX: /^[A-Z]+$/i'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('appends a REGEX line when only a SOFTREGEX value is provided', () => {
|
||||||
|
const colInfo: DataFormat = {
|
||||||
|
label: 'Some Character Column',
|
||||||
|
type: 'char',
|
||||||
|
length: '1024',
|
||||||
|
format: '$1024.'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildColInfoHtml('SOME_CHAR', colInfo, undefined, '/^[a-z]+$/')
|
||||||
|
).toBe(
|
||||||
|
'NAME: SOME_CHAR<br>LABEL: Some Character Column<br>TYPE: char<br>LENGTH: 1024<br>FORMAT: $1024.<br>REGEX: /^[a-z]+$/'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('appends separate HARDREGEX/SOFTREGEX lines when a column has both', () => {
|
||||||
|
const colInfo: DataFormat = {
|
||||||
|
label: 'Some Character Column',
|
||||||
|
type: 'char',
|
||||||
|
length: '1024',
|
||||||
|
format: '$1024.'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildColInfoHtml('SOME_CHAR', colInfo, '/^[A-Z]+$/i', '/^.{5,10}$/')
|
||||||
|
).toBe(
|
||||||
|
'NAME: SOME_CHAR<br>LABEL: Some Character Column<br>TYPE: char<br>LENGTH: 1024<br>FORMAT: $1024.<br>HARDREGEX: /^[A-Z]+$/i<br>SOFTREGEX: /^.{5,10}$/'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('omits the REGEX line when no regex rule value is provided', () => {
|
||||||
|
const colInfo: DataFormat = {
|
||||||
|
label: 'Some Character Column',
|
||||||
|
type: 'char',
|
||||||
|
length: '1024',
|
||||||
|
format: '$1024.'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(buildColInfoHtml('SOME_CHAR', colInfo)).not.toContain('REGEX:')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,9 +7,22 @@ import { DataFormat } from '../../models/sas/common/DateFormat'
|
|||||||
*/
|
*/
|
||||||
export function buildColInfoHtml(
|
export function buildColInfoHtml(
|
||||||
colName: string,
|
colName: string,
|
||||||
colInfo?: DataFormat
|
colInfo?: DataFormat,
|
||||||
|
hardRegexValue?: string,
|
||||||
|
softRegexValue?: string
|
||||||
): string {
|
): string {
|
||||||
if (!colInfo) return 'No info found'
|
if (!colInfo) return 'No info found'
|
||||||
|
|
||||||
return `NAME: ${colName}<br>LABEL: ${colInfo.label}<br>TYPE: ${colInfo.type}<br>LENGTH: ${colInfo.length}<br>FORMAT: ${colInfo.format}`
|
let html = `NAME: ${colName}<br>LABEL: ${colInfo.label}<br>TYPE: ${colInfo.type}<br>LENGTH: ${colInfo.length}<br>FORMAT: ${colInfo.format}`
|
||||||
|
|
||||||
|
// HARDREGEX/SOFTREGEX are only distinguished by name when a column has
|
||||||
|
// both - with just one rule, which one it is is already implied, so the
|
||||||
|
// generic REGEX label keeps the common case uncluttered.
|
||||||
|
if (hardRegexValue && softRegexValue) {
|
||||||
|
html += `<br>HARDREGEX: ${hardRegexValue}<br>SOFTREGEX: ${softRegexValue}`
|
||||||
|
} else if (hardRegexValue || softRegexValue) {
|
||||||
|
html += `<br>REGEX: ${hardRegexValue || softRegexValue}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return html
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ function makeRows(n) {
|
|||||||
ROUND_COL: Number((i + 1 + i / 7).toFixed(5)), // rounds on edit
|
ROUND_COL: Number((i + 1 + i / 7).toFixed(5)), // rounds on edit
|
||||||
NUMFMT_COL: 1000 + i * 12.5, // shown as EUR
|
NUMFMT_COL: 1000 + i * 12.5, // shown as EUR
|
||||||
REGEX_HARD_COL: "user@example.com", // HARDREGEX: email — starts valid
|
REGEX_HARD_COL: "user@example.com", // HARDREGEX: email — starts valid
|
||||||
REGEX_SOFT_COL: "SW1A 1AA" // SOFTREGEX: UK postcode — starts valid
|
REGEX_SOFT_COL: "SW1A 1AA", // SOFTREGEX: UK postcode — starts valid
|
||||||
|
REGEX_BOTH_COL: "ABC-123" // HARDREGEX + SOFTREGEX together — starts valid against both
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
@@ -241,6 +242,19 @@ let webouts = {
|
|||||||
DESC: "SOFTREGEX: should be a valid UK postcode, shown as a yellow warning if not, but can still submit",
|
DESC: "SOFTREGEX: should be a valid UK postcode, shown as a yellow warning if not, but can still submit",
|
||||||
LONGDESC: "",
|
LONGDESC: "",
|
||||||
COLTYPE: "{\"data\":\"REGEX_SOFT_COL\"}"
|
COLTYPE: "{\"data\":\"REGEX_SOFT_COL\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NAME: "REGEX_BOTH_COL",
|
||||||
|
LABEL: "REGEX_BOTH_COL",
|
||||||
|
FMTNAME: "",
|
||||||
|
DDTYPE: "C",
|
||||||
|
CLS_RULE: "READ",
|
||||||
|
MEMLABEL: "",
|
||||||
|
DESC: "HARDREGEX + SOFTREGEX together: must be uppercase/digits/-/_ (blocking), recommended 5-10 chars long (warning)",
|
||||||
|
LONGDESC: ""
|
||||||
|
// No COLTYPE - DcValidator falls back to a plain { data: name }
|
||||||
|
// rule when COLTYPE is absent, same shape this trivial JSON
|
||||||
|
// would produce anyway (see parseColTypeRow).
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
dqdata: [
|
dqdata: [
|
||||||
@@ -266,7 +280,9 @@ let webouts = {
|
|||||||
{ BASE_COL: "ROUND_COL", RULE_TYPE: "ROUND", RULE_VALUE: "2" },
|
{ BASE_COL: "ROUND_COL", RULE_TYPE: "ROUND", RULE_VALUE: "2" },
|
||||||
{ BASE_COL: "NUMFMT_COL", RULE_TYPE: "NUMBER_FORMAT", RULE_VALUE: '{"style":"currency","currency":"EUR"}' },
|
{ BASE_COL: "NUMFMT_COL", RULE_TYPE: "NUMBER_FORMAT", RULE_VALUE: '{"style":"currency","currency":"EUR"}' },
|
||||||
{ BASE_COL: "REGEX_HARD_COL", RULE_TYPE: "HARDREGEX", RULE_VALUE: "/[\\w.]+@[\\w]+\\.[a-z]{2,}/" },
|
{ BASE_COL: "REGEX_HARD_COL", RULE_TYPE: "HARDREGEX", RULE_VALUE: "/[\\w.]+@[\\w]+\\.[a-z]{2,}/" },
|
||||||
{ BASE_COL: "REGEX_SOFT_COL", RULE_TYPE: "SOFTREGEX", RULE_VALUE: "/[A-Z]{1,2}\\d{1,2}[A-Z]?\\s?\\d[A-Z]{2}/" }
|
{ BASE_COL: "REGEX_SOFT_COL", RULE_TYPE: "SOFTREGEX", RULE_VALUE: "/[A-Z]{1,2}\\d{1,2}[A-Z]?\\s?\\d[A-Z]{2}/" },
|
||||||
|
{ BASE_COL: "REGEX_BOTH_COL", RULE_TYPE: "HARDREGEX", RULE_VALUE: "/^[A-Z0-9_-]+$/" },
|
||||||
|
{ BASE_COL: "REGEX_BOTH_COL", RULE_TYPE: "SOFTREGEX", RULE_VALUE: "/^.{5,10}$/" }
|
||||||
],
|
],
|
||||||
dsmeta: [
|
dsmeta: [
|
||||||
{ ODS_TABLE: "ATTRIBUTES", NAME: "Data Set Name", VALUE: "DC996664.MPE_X_TEST" },
|
{ ODS_TABLE: "ATTRIBUTES", NAME: "Data Set Name", VALUE: "DC996664.MPE_X_TEST" },
|
||||||
@@ -319,7 +335,8 @@ let webouts = {
|
|||||||
{ NAME: "round_col", MAXLEN: 8 },
|
{ NAME: "round_col", MAXLEN: 8 },
|
||||||
{ NAME: "numfmt_col", MAXLEN: 8 },
|
{ NAME: "numfmt_col", MAXLEN: 8 },
|
||||||
{ NAME: "regex_hard_col", MAXLEN: 128 },
|
{ NAME: "regex_hard_col", MAXLEN: 128 },
|
||||||
{ NAME: "regex_soft_col", MAXLEN: 128 }
|
{ NAME: "regex_soft_col", MAXLEN: 128 },
|
||||||
|
{ NAME: "regex_both_col", MAXLEN: 128 }
|
||||||
],
|
],
|
||||||
query: [],
|
query: [],
|
||||||
sasdata: makeRows(100),
|
sasdata: makeRows(100),
|
||||||
@@ -341,12 +358,13 @@ let webouts = {
|
|||||||
ROUND_COL: { format: "best.", label: "ROUND_COL", length: "8", type: "num" },
|
ROUND_COL: { format: "best.", label: "ROUND_COL", length: "8", type: "num" },
|
||||||
NUMFMT_COL: { format: "best.", label: "NUMFMT_COL", length: "8", type: "num" },
|
NUMFMT_COL: { format: "best.", label: "NUMFMT_COL", length: "8", type: "num" },
|
||||||
REGEX_HARD_COL: { format: "$128.", label: "REGEX_HARD_COL", length: "128", type: "char" },
|
REGEX_HARD_COL: { format: "$128.", label: "REGEX_HARD_COL", length: "128", type: "char" },
|
||||||
REGEX_SOFT_COL: { format: "$128.", label: "REGEX_SOFT_COL", length: "128", type: "char" }
|
REGEX_SOFT_COL: { format: "$128.", label: "REGEX_SOFT_COL", length: "128", type: "char" },
|
||||||
|
REGEX_BOTH_COL: { format: "$128.", label: "REGEX_BOTH_COL", length: "128", type: "char" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sasparams: [
|
sasparams: [
|
||||||
{
|
{
|
||||||
COLHEADERS: "_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_HARDSELECT,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM,READONLY_COL,HIDDEN_COL,ROUND_COL,NUMFMT_COL,REGEX_HARD_COL,REGEX_SOFT_COL",
|
COLHEADERS: "_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_HARDSELECT,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM,READONLY_COL,HIDDEN_COL,ROUND_COL,NUMFMT_COL,REGEX_HARD_COL,REGEX_SOFT_COL,REGEX_BOTH_COL",
|
||||||
FILTER_TEXT: FILTER_TEXT,
|
FILTER_TEXT: FILTER_TEXT,
|
||||||
PKCNT: 1,
|
PKCNT: 1,
|
||||||
PK: "PRIMARY_KEY_FIELD",
|
PK: "PRIMARY_KEY_FIELD",
|
||||||
@@ -1501,7 +1519,7 @@ let webouts = {
|
|||||||
// MPE_X_TEST keeps matching the excel upload fixtures (which predate these
|
// MPE_X_TEST keeps matching the excel upload fixtures (which predate these
|
||||||
// columns). The demo tables are built lazily (below) so we don't clone the large
|
// columns). The demo tables are built lazily (below) so we don't clone the large
|
||||||
// MPE_X_TEST payload unless MPE_X_NEW is actually requested.
|
// MPE_X_TEST payload unless MPE_X_NEW is actually requested.
|
||||||
const RULE_DEMO_COLS = ['SOME_HARDSELECT', 'READONLY_COL', 'HIDDEN_COL', 'ROUND_COL', 'NUMFMT_COL', 'REGEX_HARD_COL', 'REGEX_SOFT_COL']
|
const RULE_DEMO_COLS = ['SOME_HARDSELECT', 'READONLY_COL', 'HIDDEN_COL', 'ROUND_COL', 'NUMFMT_COL', 'REGEX_HARD_COL', 'REGEX_SOFT_COL', 'REGEX_BOTH_COL']
|
||||||
|
|
||||||
function stripRuleCols(t) {
|
function stripRuleCols(t) {
|
||||||
const uc = new Set(RULE_DEMO_COLS)
|
const uc = new Set(RULE_DEMO_COLS)
|
||||||
|
|||||||
Reference in New Issue
Block a user