Compare commits

..

15 Commits

Author SHA1 Message Date
sead 4ea604f9fb feat(editor): add READONLY, HIDDEN, ROUND and NUMBER_FORMAT validation rules
Build / Build-and-ng-test (pull_request) Failing after 15m15s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 20m15s
2026-06-05 21:25:13 +02:00
sead 9d97bf7ea1 fix(handsontable): restore dark mode for v17
Lighthouse Checks / lighthouse (pull_request) Failing after 17m59s
Build / Build-and-ng-test (pull_request) Failing after 17m59s
Build / Build-and-test-development (pull_request) Has been skipped
2026-06-05 14:12:11 +02:00
sead eb015d712b revert(editor): DISPLAY_VALUE change
Build / Build-and-ng-test (pull_request) Failing after 16m30s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 31m28s
Partially revert changes from 51071b463b
2026-05-26 22:08:20 +02:00
sead 1d04f4a42c refactor(editor): add bulk validation only on HARDSELECT_HOOK; skip SOFTSELECT_HOOK
Build / Build-and-ng-test (pull_request) Failing after 17m35s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 32m35s
2026-05-26 21:57:30 +02:00
sead 11ee49a57a feat(editor): validate autofilled cells; fix paste validation lag
Build / Build-and-ng-test (pull_request) Successful in 3m33s
Lighthouse Checks / lighthouse (pull_request) Failing after 27m56s
Build / Build-and-test-development (pull_request) Failing after 34m22s
2026-05-26 17:09:35 +02:00
sead 609731ff99 feat(editor): paste-validation overlay with cancel and confirm
Build / Build-and-ng-test (pull_request) Failing after 16m23s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 31m23s
2026-05-26 14:38:41 +02:00
sead d6cb32ed25 refactor(licensing): expand protocl text
Build / Build-and-ng-test (pull_request) Failing after 17m14s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 32m12s
2026-05-26 11:47:52 +02:00
sead 3668a7426f fix(licensing): add protocol info
Build / Build-and-ng-test (pull_request) Successful in 3m32s
Build / Build-and-test-development (pull_request) Failing after 20m50s
Lighthouse Checks / lighthouse (pull_request) Failing after 29m24s
Close #178
2026-05-26 11:40:40 +02:00
sead cc82dcaafe refactor(mocks): replace string webouts with JS objects
Build / Build-and-ng-test (pull_request) Failing after 17m42s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 28m24s
2026-05-26 11:06:50 +02:00
sead ea03bdecc5 fix(editor): await dynamic validation on paste; defer spinner 2026-05-26 10:48:13 +02:00
sead 51071b463b fix(editor): cancelEdit will reset cell's valid state 2026-05-25 16:55:18 +02:00
sead ac0bd10212 fix(handsontable): horizontal scrollbar in dropdown 2026-05-25 13:59:24 +02:00
sead 1b73e355b7 fix: migrate handsontables to v17 2026-05-25 13:29:27 +02:00
sead b661580c60 fix: validate pasted values 2026-05-22 22:10:58 +02:00
sead dc4e07a692 fix: viewLibs now fires only once, libPromise shared between the calls 2026-05-22 21:57:53 +02:00
60 changed files with 4839 additions and 7199 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ const check = (cwd) => {
onlyAllow:
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
excludePackages:
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;handsontable@^16.0.1;handsontable@16.2.0;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;hyperformula@3.2.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;@handsontable/angular-wrapper@17.1.0;handsontable@^16.0.1;handsontable@16.2.0;handsontable@17.1.0;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;hyperformula@3.2.0;hyperformula@3.3.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
},
(error, json) => {
if (error) {
+17 -35
View File
@@ -20,7 +20,7 @@
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
"@clr/icons": "^13.0.2",
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
"@handsontable/angular-wrapper": "16.0.1",
"@handsontable/angular-wrapper": "^17.1.0",
"@sasjs/adapter": "^4.17.0",
"@sasjs/utils": "^3.5.3",
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
@@ -32,7 +32,7 @@
"crypto-js": "^4.2.0",
"d3-graphviz": "^5.0.2",
"fs-extra": "^7.0.1",
"handsontable": "^16.0.1",
"handsontable": "^17.1.0",
"https-browserify": "1.0.0",
"hyperformula": "^2.5.0",
"iconv-lite": "^0.5.0",
@@ -4349,23 +4349,17 @@
}
},
"node_modules/@handsontable/angular-wrapper": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/@handsontable/angular-wrapper/-/angular-wrapper-16.0.1.tgz",
"integrity": "sha512-1yK5ES5l6+uG3KjXvfd9L0RupfPC8Rq5AR0D8tYBAG+Fyhr7oVHKbBONNSS/nzZHifgr/YLrnAvutQ+EZb0FdA==",
"version": "17.1.0",
"resolved": "https://registry.npmjs.org/@handsontable/angular-wrapper/-/angular-wrapper-17.1.0.tgz",
"integrity": "sha512-5jUCb4E1eZcy/CJ3T39/cxauy9NoQh+wNWTDW9ZhIIudr/00HvfJlcEozN5RWSYxfGNw71prVM2y16DSd+ct9A==",
"license": "SEE LICENSE IN LICENSE.txt",
"optionalDependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/animations": ">=16.0.0",
"@angular/common": ">=16.0.0",
"@angular/compiler": ">=16.0.0",
"@angular/core": ">=16.0.0",
"@angular/forms": ">=16.0.0",
"@angular/platform-browser": ">=16.0.0",
"@angular/platform-browser-dynamic": ">=16.0.0",
"@angular/router": ">=16.0.0",
"handsontable": "^16.0.0"
"@angular/core": ">=16.0.0 <22.0.0",
"handsontable": "^17.0.0",
"rxjs": "^7.0.0"
}
},
"node_modules/@handsontable/pikaday": {
@@ -12506,9 +12500,9 @@
}
},
"node_modules/dompurify": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz",
"integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==",
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.5.tgz",
"integrity": "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -14551,13 +14545,12 @@
}
},
"node_modules/handsontable": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/handsontable/-/handsontable-16.2.0.tgz",
"integrity": "sha512-4zhMQON9DPyip/6YIPH2G7jN+QEJ0uabCZruhrhOqTqr3Qf/FDjsTInUaEzMCmhhdii5MbA6PGyLfUad6t1sXA==",
"version": "17.1.0",
"resolved": "https://registry.npmjs.org/handsontable/-/handsontable-17.1.0.tgz",
"integrity": "sha512-3afpoGQT04i11IYge4gZNHp4Io3RFoLi+vVk4yTc2oHGjuBJBhjVVlTnOcYM8N9Ay1ikjSh4T3ZiwFVYEsgJrA==",
"license": "SEE LICENSE IN LICENSE.txt",
"dependencies": {
"@handsontable/pikaday": "^1.0.0",
"core-js": "^3.37.0",
"dompurify": "^3.1.7",
"moment": "2.30.1",
"numbro": "2.5.0"
@@ -14566,21 +14559,10 @@
"hyperformula": "^3.0.0"
}
},
"node_modules/handsontable/node_modules/core-js": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/handsontable/node_modules/hyperformula": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/hyperformula/-/hyperformula-3.2.0.tgz",
"integrity": "sha512-2vzQKKVMDPLsubZJb0JJWT/DhrkgIjsWj40Z9BIUVT6Jkl/YM5VtkLOP3agCieqW9HuqnXlWc+Vi+7XzQuC1Nw==",
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/hyperformula/-/hyperformula-3.3.0.tgz",
"integrity": "sha512-Mkc7AxP9WQDM4xHo1/XpAwzcpMLkwMand4HO9rfRzP502TBzoR1Z96zoCzRrwXodLSVyh+MD7FvzRVALDxmRFQ==",
"license": "GPL-3.0-only",
"optional": true,
"dependencies": {
+3 -3
View File
@@ -23,7 +23,7 @@
"watch": "ng test watch=true",
"pree2e": "webdriver-manager update",
"e2e": "protractor protractor.config.js",
"postinstall": "node ./src/version.ts && npm run add-githook && node ./scripts/strip-clr-base64-fonts.mjs",
"postinstall": "node ./src/version.ts && npm run add-githook && node ./scripts/strip-clr-base64-fonts.mjs && node ./scripts/gen-hot-icons.mjs",
"add-githook": "[ -d ../.git ] && git config core.hooksPath ./.git-hooks || true",
"cypress": "cypress open",
"cy:run": "cypress run",
@@ -50,7 +50,7 @@
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
"@clr/icons": "^13.0.2",
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
"@handsontable/angular-wrapper": "16.0.1",
"@handsontable/angular-wrapper": "^17.1.0",
"@sasjs/adapter": "^4.17.0",
"@sasjs/utils": "^3.5.3",
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
@@ -62,7 +62,7 @@
"crypto-js": "^4.2.0",
"d3-graphviz": "^5.0.2",
"fs-extra": "^7.0.1",
"handsontable": "^16.0.1",
"handsontable": "^17.1.0",
"https-browserify": "1.0.0",
"hyperformula": "^2.5.0",
"iconv-lite": "^0.5.0",
+69
View File
@@ -0,0 +1,69 @@
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'fs'
import { resolve, join } from 'path'
import { createRequire } from 'module'
/**
* Generate static SVG assets + an SCSS partial that re-applies HOT v17 classic
* theme icons via real URLs (not data: URIs).
*
* Why: deployed app runs under CSP `img-src 'self'`. HOT v17's classic theme
* embeds icons as `data:image/svg+xml,...` in `-webkit-mask-image` rules, which
* the CSP blocks. We switch to `ht-theme-classic-no-icons.min.css` and re-add
* the icon rules pointing at same-origin SVG files emitted from this script.
*
* Inputs (HOT's own modules, so semantic names + selector list track upstream):
* handsontable/themes/theme/classic → { classicTheme: { icons } }
* handsontable/themes/static/variables/helpers/iconsMap → iconsMap(icons, themePrefix)
*
* Outputs:
* client/src/assets/hot-icons/<kebab-name>.svg
* client/src/_hot-icons.scss
*
* Idempotent: clears the output dir and rewrites both outputs each run.
* Skips silently if handsontable isn't installed yet (pre-install runs).
*/
const require = createRequire(import.meta.url)
const ASSETS_DIR = resolve('src/assets/hot-icons')
const SCSS_OUT = resolve('src/_hot-icons.scss')
const ASSET_URL_PREFIX = './assets/hot-icons/'
const themePath = resolve('node_modules/handsontable/themes/theme/classic.js')
const mapPath = resolve('node_modules/handsontable/themes/static/variables/helpers/iconsMap.js')
if (!existsSync(themePath) || !existsSync(mapPath)) {
console.log('skip: handsontable theme modules not found (likely pre-install run)')
process.exit(0)
}
const { classicTheme } = require(themePath)
const { iconsMap } = require(mapPath)
const icons = classicTheme.icons
const cssTemplate = iconsMap(icons, 'ht-theme-classic')
const kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
rmSync(ASSETS_DIR, { recursive: true, force: true })
mkdirSync(ASSETS_DIR, { recursive: true })
const writeMap = {}
for (const [name, dataUri] of Object.entries(icons)) {
if (typeof dataUri !== 'string' || !dataUri.startsWith('data:image/svg+xml')) continue
const decoded = decodeURIComponent(dataUri.replace(/^data:image\/svg\+xml(;charset=utf-8)?,/, ''))
const fname = kebab(name) + '.svg'
writeFileSync(join(ASSETS_DIR, fname), decoded)
writeMap[dataUri] = ASSET_URL_PREFIX + fname
}
let scss = cssTemplate
for (const [uri, url] of Object.entries(writeMap)) {
scss = scss.split(`url("${uri}")`).join(`url("${url}")`)
}
const header = '/* Auto-generated by scripts/gen-hot-icons.mjs — do not edit by hand.\n' +
' Regenerated on postinstall; rerun manually via `node scripts/gen-hot-icons.mjs`. */\n\n'
writeFileSync(SCSS_OUT, header + scss + '\n')
console.log(`hot-icons: wrote ${Object.keys(writeMap).length} SVGs + ${SCSS_OUT}`)
+238
View File
@@ -0,0 +1,238 @@
/* Auto-generated by scripts/gen-hot-icons.mjs — do not edit by hand.
Regenerated on postinstall; rerun manually via `node scripts/gen-hot-icons.mjs`. */
[class*=ht-theme-classic] .htDropdownMenu table tbody tr td.htSubmenu .htItemWrapper::after,
[class*=ht-theme-classic] .htContextMenu table tbody tr td.htSubmenu .htItemWrapper::after,
[class*=ht-theme-classic] .htFiltersConditionsMenu table tbody tr td.htSubmenu .htItemWrapper::after,
[class*=ht-theme-classic] .pika-single .pika-next {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-right.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .pika-single .pika-prev {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-left.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-page-size-section__select-wrapper::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-down.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .changeType::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/select-arrow.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .htUISelectCaption::after,
.htAutocompleteArrow::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/select-arrow.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .columnSorting.sortAction.ascending::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-narrow-up.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .columnSorting.sortAction.descending::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-narrow-down.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-page-navigation-section .ht-page-first::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-left-with-bar.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] [dir="rtl"] .ht-page-navigation-section .ht-page-first::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-right-with-bar.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-page-navigation-section .ht-page-prev::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-left.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] [dir="rtl"] .ht-page-navigation-section .ht-page-prev::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-right.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-page-navigation-section .ht-page-next::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-right.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] [dir="rtl"] .ht-page-navigation-section .ht-page-next::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-left.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-page-navigation-section .ht-page-last::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-right-with-bar.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] [dir="rtl"] .ht-page-navigation-section .ht-page-last::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-left-with-bar.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .htDropdownMenu table tbody tr td .htItemWrapper span.selected::after,
[class*=ht-theme-classic] .htContextMenu table tbody tr td .htItemWrapper span.selected::after,
[class*=ht-theme-classic] .htFiltersConditionsMenu table tbody tr td .htItemWrapper span.selected::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/check.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .htCheckboxRendererInput {
appearance: none;
}
[class*=ht-theme-classic] .htCheckboxRendererInput::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/checkbox.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] th.beforeHiddenColumn::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/caret-hidden-left.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] th.afterHiddenColumn::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/caret-hidden-right.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] th.beforeHiddenRow::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/caret-hidden-up.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] th.afterHiddenRow::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/caret-hidden-down.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .collapsibleIndicator::before,
[class*=ht-theme-classic] .ht_nestingButton::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/collapse-off.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .collapsibleIndicator.collapsed::before,
[class*=ht-theme-classic] .ht_nestingButton.ht_nestingExpand::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/collapse-on.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .htUIRadio > input[type="radio"]::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/radio.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-multi-select-chip-remove::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/chip-close.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-notification__close::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/chip-close.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-multi-select-editor-search-icon {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/search.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-multi-select-editor-item-selected input::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/checkbox.svg");
background-color: currentColor;
}
@@ -101,7 +101,7 @@ export class EditRecordComponent implements OnInit {
let format = cellValidation ? cellValidation.dateFormat : ''
if (this.currentRecord)
this.currentRecord[colKey] = moment(date).format(format)
this.currentRecord[colKey] = moment(date).format(format as string)
}
/**
@@ -873,3 +873,17 @@
</app-dataset-info>
<app-viewboxes [(viewboxModal)]="viewboxes"></app-viewboxes>
<app-confirm-modal
[open]="confirmModal.open"
[title]="confirmModal.title"
[message]="confirmModal.message"
(result)="onConfirmModalResult($event)"
></app-confirm-modal>
<app-bulk-validation-modal
[open]="bulkValidation.active"
[done]="bulkValidation.done"
[total]="bulkValidation.total"
(cancel)="cancelBulkValidation({ revert: true })"
></app-bulk-validation-modal>
+383 -64
View File
@@ -44,6 +44,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 { excelRound } from '../shared/dc-validator/utils/excelRound'
import { isEmpty } from '../shared/dc-validator/utils/isEmpty'
import { globals } from '../_globals'
import { UploadStaterComponent } from './components/upload-stater/upload-stater.component'
@@ -133,7 +134,9 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
licenseKey: this.hotTable.licenseKey,
readOnly: this.hotTable.readOnly,
copyPaste: this.hotTable.copyPaste,
contextMenu: true
contextMenu: true,
className: 'htDark',
theme: 'ht-theme-classic'
}
}
@@ -357,7 +360,29 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
* Hash/values table used for dynamic cell validation
*/
public cellValidationSource: CellValidationSource[] = []
public validationTableLimit = 20
public validationTableLimit = 100
// Incremented on cancel/edit-exit so in-flight dynamic-validation
// responses can detect they should drop their post-response work.
private validationEpoch = 0
// Cells currently showing the loading spinner renderer (keyed `r,c`),
// so cancelBulkValidation can reset them.
private pendingSpinnerCells = new Set<string>()
// State for the bulk-validation progress banner (paste / autofill).
public bulkValidation: {
active: boolean
done: number
total: number
} = { active: false, done: 0, total: 0 }
// Confirm-modal state used to gate large paste validations.
public confirmModal: {
open: boolean
title: string
message: string
} = { open: false, title: '', message: '' }
private confirmModalResolver: ((v: boolean) => void) | null = null
public extendedCellValidationFields: {
DISPLAY_INDEX: number
EXTRA_COL_NAME: number
@@ -963,6 +988,8 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
}
cancelEdit() {
this.cancelBulkValidation({ revert: false })
this.toggleHotPlugin('contextMenu', false)
this.cellValidationSource = []
@@ -992,7 +1019,8 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
false
)
hot.validateRows(this.modifedRowsIndexes)
this.modifedRowsIndexes = []
hot.validateCells()
// this.editRecordListeners();
for (const sortConfig of sortConfigs) {
columnSorting.sort(sortConfig)
@@ -1001,6 +1029,160 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.checkRowLimit()
}
/**
* Stop the bulk-validation flow (paste / autofill): invalidate in-flight
* responses via the epoch counter, reset spinner cells, hide the banner,
* and (when triggered from the banner's Cancel button) undo the change.
*/
public cancelBulkValidation(opts: { revert?: boolean } = {}) {
const wasActive = this.bulkValidation.active
// Invalidate any in-flight dynamicCellValidation responses. Note:
// sasService.request has no abort signal, so the network request itself
// keeps running — we only drop the response handling.
this.validationEpoch++
// Reset any cells still showing the loading spinner renderer.
const hot = this.hotInstance
if (hot && this.pendingSpinnerCells.size > 0) {
for (const key of this.pendingSpinnerCells) {
const [rStr, cStr] = key.split(',')
const r = Number(rStr)
const c = Number(cStr)
hot.setCellMeta(r, c, 'renderer', noSpinnerRenderer)
}
this.pendingSpinnerCells.clear()
}
// Drop placeholder entries (values still empty — request was cancelled).
this.cellValidationSource = this.cellValidationSource.filter(
(entry) => !entry.pending || entry.values.length > 0
)
this.bulkValidation = {
active: false,
done: 0,
total: 0
}
if (wasActive && opts.revert && hot) {
this.undoLastChange(hot)
}
if (hot) hot.render()
}
private undoLastChange(hot: Handsontable): void {
const plugin = hot.getPlugin('undoRedo') as unknown as
| { isUndoAvailable(): boolean; undo(): void }
| undefined
if (plugin?.isUndoAvailable()) plugin.undo()
}
/**
* Drive dynamic-source load + validation for cells that were bulk-filled
* (paste or autofill). HARDSELECT_HOOK / SOFTSELECT_HOOK columns need a SAS
* roundtrip; non-hook cells just need HOT's static validators. Caps backend
* concurrency, uses an epoch so a cancelled run can't mutate later state,
* and gates >3-cell runs behind a confirm modal.
*/
private async runBulkValidation(
hot: Handsontable,
ranges: Array<{
startRow: number
startCol: number
endRow: number
endCol: number
}>,
source: 'paste' | 'autofill'
): Promise<void> {
const rows = new Set<number>()
const hookTargets: Array<{ r: number; c: number }> = []
const hookCols = new Set<string>()
for (const range of ranges) {
for (let r = range.startRow; r <= range.endRow; r++) {
for (let c = range.startCol; c <= range.endCol; c++) {
rows.add(r)
const colKey = hot.colToProp(c) as string
if (this.dcValidator?.hasDqRules(colKey, ['HARDSELECT_HOOK'])) {
hookCols.add(colKey)
hookTargets.push({ r, c })
}
}
}
}
// No hook columns → HOT's own setDataAtCell → validateChanges pass
// (triggered by populateFromArray) already validates against the new
// value and paints htInvalid. A second validateRows here would race:
// it runs sync inside afterPaste/afterAutofill BEFORE applyChanges
// writes the data, captures the stale old value, and overwrites
// cellProperties.valid back to true — causing a 1-action lag.
if (hookTargets.length === 0) return
if (hookTargets.length === 1) {
const { r, c } = hookTargets[0]
await this.dynamicCellValidation(r, c)
hot.validateRows([...rows], () => hot.render())
return
}
if (hookTargets.length > 3) {
const colsList = [...hookCols].join(', ')
const ok = await this.showConfirmModal(
`Confirm ${source} validation`,
`You are about to trigger ${hookTargets.length} backend SAS request(s) for columns: ${colsList}. Do you wish to proceed?`
)
if (!ok) {
this.undoLastChange(hot)
return
}
}
const epoch = this.validationEpoch
this.bulkValidation = {
active: true,
done: 0,
total: hookTargets.length
}
const CONCURRENCY = 2
let idx = 0
await Promise.all(
Array.from({ length: CONCURRENCY }, async () => {
while (idx < hookTargets.length) {
if (epoch !== this.validationEpoch) return
const { r, c } = hookTargets[idx++]
await this.dynamicCellValidation(r, c, { skipRender: true })
if (epoch === this.validationEpoch) {
this.bulkValidation.done++
}
}
})
)
if (epoch !== this.validationEpoch) return
this.bulkValidation = {
...this.bulkValidation,
active: false
}
hot.validateRows([...rows], () => hot.render())
}
private showConfirmModal(title: string, message: string): Promise<boolean> {
this.confirmModal = { open: true, title, message }
return new Promise<boolean>((resolve) => {
this.confirmModalResolver = resolve
})
}
public onConfirmModalResult(value: boolean) {
const resolver = this.confirmModalResolver
this.confirmModalResolver = null
this.confirmModal = { ...this.confirmModal, open: false }
if (resolver) resolver(value)
}
timesClicked = 0
public hotClicked() {
if (this.timesClicked === 1 && this.hotTable.readOnly) {
@@ -1969,18 +2151,26 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
* @param row handsontable row
* @param column handsontable column
*/
public dynamicCellValidation(row: number, column: number) {
if (this.dynamicCellValidationDisabled(row, column)) return
public async dynamicCellValidation(
row: number,
column: number,
opts?: { skipRender?: boolean },
retried = false
): Promise<void> {
if (this.dynamicCellValidationDisabled(row, column))
return Promise.resolve()
const hot = this.hotInstance
const cellMeta = hot.getCellMeta(row, column)
if (cellMeta.readOnly) return
if (cellMeta.readOnly) return Promise.resolve()
const cellData = hot.getDataAtCell(row, column)
const clickedRow = this.helperService.deepClone(this.dataSource[row])
const clickedColumnKey = Object.keys(clickedRow)[column]
const skipRender = !!opts?.skipRender
const myEpoch = this.validationEpoch
/**
* We will hash the row (without current column) so later we check if hash is the same
@@ -2001,6 +2191,22 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
* Set the values for found hash.
*/
if (validationSourceIndex > -1) {
// In-flight dedup: another call with the same hash is mid-request.
// Wait for it then re-enter once so we walk the populated cache-hit
// path instead of validating against the empty placeholder.
const inFlight = this.cellValidationSource[validationSourceIndex].pending
if (inFlight && !retried) {
try {
await inFlight
} catch {
/* swallowed — original caller handles */
}
if (myEpoch !== this.validationEpoch) return
return this.dynamicCellValidation(row, column, opts, true)
}
}
if (validationSourceIndex > -1) {
let colSource = this.cellValidationSource[
validationSourceIndex
@@ -2076,14 +2282,12 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
cellHadSource && cellHasValue
)
hot.render()
if (!skipRender) hot.render()
})
}
/**
* Send request to sas.
*/
if (validationSourceIndex < 0) {
} else if (validationSourceIndex < 0) {
/**
* Send request to sas.
*/
const data = {
SASControlTable: [
{
@@ -2115,21 +2319,53 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
count: this.cellValidationSource.length + 1
})
hot.setCellMeta(row, column, 'renderer', spinnerRenderer)
this.currentEditRecordLoadings.push(column)
hot.render()
this.sasService
const spinnerKey = `${row},${column}`
// Defer the spinner renderer so the click event finishes settling
// HOT's focus catcher before we replace td.innerHTML. Without this
// defer, clicking the cell loses focus immediately.
// Skip the spinner entirely if SAS responds before this fires —
// avoids the brief flicker users were seeing on fast responses.
// Also skip during paste (skipRender) — the progress banner handles
// visual feedback and cell spinners would just flicker.
const spinnerTimeout: ReturnType<typeof setTimeout> | null = skipRender
? null
: setTimeout(() => {
hot.setCellMeta(row, column, 'renderer', spinnerRenderer)
this.pendingSpinnerCells.add(spinnerKey)
hot.render()
}, 150)
const pendingPromise = this.sasService
.request('editors/getdynamiccolvals', data, undefined, {
suppressSuccessAbortModal: true,
suppressErrorAbortModal: true
})
.then((res: RequestWrapperResponse) => {
.then(async (res: RequestWrapperResponse) => {
if (spinnerTimeout) clearTimeout(spinnerTimeout)
this.pendingSpinnerCells.delete(spinnerKey)
// Cancelled mid-flight — drop the placeholder entry so future
// calls don't see an empty cache hit, and skip all UI work.
if (myEpoch !== this.validationEpoch) {
const idx = this.cellValidationSource.findIndex(
(e) => e.hash === hashedRow
)
if (idx > -1) this.cellValidationSource.splice(idx, 1)
return
}
const colSource = res.adapterResponse.dynamic_values.map(
(el: any) => el[this.cellValidationFields.RAW_VALUE]
)
this.currentEditRecordLoadings.splice(
this.currentEditRecordLoadings.indexOf(column),
1
)
if (colSource.length > 0) {
const validationSourceIndex = this.cellValidationSource.findIndex(
(entry: CellValidationSource) => entry.hash === hashedRow
@@ -2141,49 +2377,37 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
row: row,
col: column,
values: res.adapterResponse.dynamic_values,
extended_values: res.adapterResponse.dynamic_extended_values
extended_values: res.adapterResponse.dynamic_extended_values,
pending: undefined
}
}
//Removing the spinner from cell, so validation not fail
hot.setCellMeta(row, column, 'renderer', noSpinnerRenderer)
this.currentEditRecordLoadings.splice(
this.currentEditRecordLoadings.indexOf(column),
1
)
hot.deselectCell()
hot.render()
/**
* `cells` function of hot settings is remembering the old state of component
* we need to update it here after we set new `cellValidationSource` (validation lookup hash table) values
* so that it will check those values to decide whether numeric cells should be
* converted to the dropdown
* In the case that the original value is not included in the newly created cell dropdown
* and validation type is HARDSELECT, the cell shoud be red
*/
hot.batch(() => {
/**
* In the case that the original value is not included in the newly created cell dropdown
* and validation type is HARDSELECT, the cell shoud be red
*/
await new Promise<void>((resolve) =>
setTimeout(() => {
this.reSetCellValidationValues(true, row)
hot.render()
hot.validateRows([row])
if (!skipRender) {
hot.render()
hot.validateRows([row])
}
resolve()
}, 100)
})
)
} else {
if (!skipRender) {
hot.setCellMeta(row, column, 'renderer', noSpinnerRenderer)
hot.render()
}
const idx = this.cellValidationSource.findIndex(
(e) => e.hash === hashedRow
)
if (idx > -1) this.cellValidationSource[idx].pending = undefined
}
//Removing the spinner from cell, so validation not fail
hot.setCellMeta(row, column, 'renderer', noSpinnerRenderer)
this.currentEditRecordLoadings.splice(
this.currentEditRecordLoadings.indexOf(column),
1
)
hot.deselectCell()
hot.render()
/**
* If hash table limit reached, remove the oldest element.
* Oldest element is element with lowest `count` number.
@@ -2199,18 +2423,25 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
}
})
.catch((err: any) => {
if (spinnerTimeout) clearTimeout(spinnerTimeout)
this.pendingSpinnerCells.delete(spinnerKey)
const currentRowHashIndex = this.cellValidationSource.findIndex(
(x) => x.hash === hashedRow
)
this.cellValidationSource.splice(currentRowHashIndex, 1)
hot.batch(() => {
// Render error icon inside a cell
hot.setCellMeta(row, column, 'renderer', errorRenderer)
if (myEpoch !== this.validationEpoch) return
hot.render()
})
if (!skipRender) {
hot.batch(() => {
// Render error icon inside a cell
hot.setCellMeta(row, column, 'renderer', errorRenderer)
hot.render()
})
}
//Stop edit record modal loading spinner
this.currentEditRecordLoadings.splice(
@@ -2223,8 +2454,10 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
// After waiting time remove the error icon from cell and edit record modal field
setTimeout(() => {
hot.setCellMeta(row, column, 'renderer', noSpinnerRenderer)
hot.render()
if (!skipRender) {
hot.setCellMeta(row, column, 'renderer', noSpinnerRenderer)
hot.render()
}
//Remove error icon on the edit record modal field
this.currentEditRecordErrors.splice(
@@ -2237,8 +2470,19 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.loggerService.log('getdynamiccolvals error:', err)
})
const entryIdx = this.cellValidationSource.findIndex(
(e) => e.hash === hashedRow
)
if (entryIdx > -1) {
this.cellValidationSource[entryIdx].pending = pendingPromise
}
return pendingPromise
}
}
return Promise.resolve()
}
checkEmptyRowWhenFilter() {
@@ -2938,6 +3182,31 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.setCellFilter(true)
})
// ROUND: round numeric values Excel-style before they are written.
// Mutating `changes` in place (rather than setDataAtRowProp) avoids
// re-entrancy and uniformly covers edit, paste and autofill.
hot.addHook('beforeChange', (changes: any[]) => {
if (!changes) return
for (const change of changes) {
if (!change) continue
const [, prop, , newValue] = change
const colName =
typeof prop === 'string'
? prop
: (hot.colToProp(prop as number) as string)
const digits = this.dcValidator?.getRoundDigits(colName)
if (digits === undefined) continue
const num = Number(newValue)
if (newValue !== null && newValue !== '' && !isNaN(num)) {
change[3] = excelRound(num, digits)
}
}
})
hot.addHook('afterChange', (source: any, change: any) => {
if (change === 'edit') {
const hot = this.hotInstance
@@ -2956,6 +3225,38 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
}
})
hot.addHook('afterPaste', async (_data: any, coords: any) => {
// In read-only mode HOT discards the paste itself, so nothing to validate.
if (this.hotTable.readOnly) return
const ranges = (coords as any[]).map((r) => ({
startRow: r.startRow,
startCol: r.startCol,
endRow: r.endRow,
endCol: r.endCol
}))
await this.runBulkValidation(hot, ranges, 'paste')
})
hot.addHook(
'afterAutofill',
async (_fillData: any, _sourceRange: any, targetRange: any) => {
if (this.hotTable.readOnly) return
const { from, to } = targetRange
await this.runBulkValidation(
hot,
[
{
startRow: Math.min(from.row, to.row),
startCol: Math.min(from.col, to.col),
endRow: Math.max(from.row, to.row),
endCol: Math.max(from.col, to.col)
}
],
'autofill'
)
}
)
hot.addHook('afterRender', (isForced: boolean) => {
this.eventService.dispatchEvent('resize')
@@ -3020,21 +3321,39 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
}
)
hot.addHook('beforePaste', (data: any, cords: any) => {
const startCol = cords[0].startCol
// We iterate trough pasting data to convert to numbers if needed
data[0] = data[0].map((value: any, index: number) => {
// Coerce numeric-column values from string → number so length-check
// and downstream validators see the correct type on the first pass
// (string "3.5" passes `Number(value) === value`, masking float-in-
// short-num errors until the next edit).
const coerceNumericRow = (
row: any[],
startCol: number,
rowMaxLen?: number
): any[] =>
row.map((value: any, index: number) => {
if (rowMaxLen !== undefined && index >= rowMaxLen) return value
const colName = this.columnHeader[startCol + index]
const isColNum = this.$dataFormats?.vars[colName]?.type === 'num'
const specialMissing = isSpecialMissing(value)
if (isColNum && !isNaN(value) && !specialMissing) value = value * 1
return value
})
hot.addHook('beforePaste', (data: any, cords: any) => {
const startCol = cords[0].startCol
for (let r = 0; r < data.length; r++) {
data[r] = coerceNumericRow(data[r], startCol)
}
})
hot.addHook(
'beforeAutofill',
(selectionData: any[][], sourceRange: any) => {
const startCol = sourceRange.from.col
return selectionData.map((row) => coerceNumericRow(row, startCol))
}
)
hot.addHook('afterRemoveRow', () => {
this.checkRowLimit()
})
@@ -0,0 +1,88 @@
import Handsontable from 'handsontable'
import { makeNumberFormatRenderer } from './renderers.utils'
describe('makeNumberFormatRenderer', () => {
it('renders a numeric cell as EUR currency without changing the value', () => {
const container = document.createElement('div')
document.body.appendChild(container)
const hot = new Handsontable(container, {
data: [{ amt: 1025 }],
columns: [
{
data: 'amt',
type: 'numeric',
renderer: makeNumberFormatRenderer(
'{"style":"currency","currency":"EUR"}'
)
}
],
licenseKey: 'non-commercial-and-evaluation'
})
hot.render()
const td = hot.getCell(0, 0)
// Display is formatted as currency...
expect(td?.textContent).toContain('€')
expect(td?.textContent).toContain('1,025')
// ...but the stored value is untouched
expect(hot.getDataAtCell(0, 0)).toEqual(1025)
hot.destroy()
container.remove()
})
it('is overridden by numbro numericFormat (why DcValidator clears it for NUMBER_FORMAT cols)', () => {
// Regression note: on a `type: 'numeric'` column, a `numericFormat` makes
// HOT re-render via numbro and drop our currency symbol. DcValidator clears
// numericFormat on NUMBER_FORMAT columns so the Intl renderer wins.
const container = document.createElement('div')
document.body.appendChild(container)
const hot = new Handsontable(container, {
data: [{ amt: 1025 }],
columns: [
{
data: 'amt',
type: 'numeric',
numericFormat: { pattern: '0,0', culture: 'en-US' },
renderer: makeNumberFormatRenderer(
'{"style":"currency","currency":"EUR"}'
)
}
],
licenseKey: 'non-commercial-and-evaluation'
})
hot.render()
const td = hot.getCell(0, 0)
expect(td?.textContent).not.toContain('€')
hot.destroy()
container.remove()
})
it('falls back to a plain number when options JSON is invalid', () => {
const container = document.createElement('div')
document.body.appendChild(container)
const hot = new Handsontable(container, {
data: [{ amt: 1025 }],
columns: [
{
data: 'amt',
type: 'numeric',
renderer: makeNumberFormatRenderer('not json')
}
],
licenseKey: 'non-commercial-and-evaluation'
})
hot.render()
const td = hot.getCell(0, 0)
expect(td?.textContent).not.toContain('€')
hot.destroy()
container.remove()
})
})
@@ -1,3 +1,57 @@
import Handsontable from 'handsontable'
/**
* Builds a display-only HOT renderer that formats numeric cell values using
* Intl.NumberFormat. The stored/submitted value is never changed — only the
* rendered text. `ruleValue` is a JSON string of Intl.NumberFormat options
* (e.g. '{"style":"currency","currency":"EUR","minimumFractionDigits":2}').
*
* Falls back to the plain text renderer when the JSON is invalid, the options
* are rejected by Intl.NumberFormat, or the value is not a finite number.
*/
export const makeNumberFormatRenderer = (ruleValue?: string) => {
let formatter: Intl.NumberFormat | null = null
try {
const options = ruleValue ? JSON.parse(ruleValue) : {}
formatter = new Intl.NumberFormat(window.navigator.language, options)
} catch (e) {
console.warn(
`NUMBER_FORMAT - invalid Intl.NumberFormat options: ${ruleValue}`
)
formatter = null
}
const baseRenderer = Handsontable.renderers.getRenderer('text')
return (
instance: any,
td: any,
row: number,
col: number,
prop: string | number,
value: any,
cellProperties: any
) => {
// Render via the base text renderer first to preserve cell styling/classes
// (readOnly, alignment, etc.), then override the displayed text.
baseRenderer(instance, td, row, col, prop, value, cellProperties)
const num = Number(value)
if (
formatter &&
value !== null &&
value !== undefined &&
value !== '' &&
!isNaN(num)
) {
td.textContent = formatter.format(num)
}
return td
}
}
/**
* Custom renderer for HOT cell
* Used to show error icon
@@ -34,6 +34,8 @@
</p>
</ng-container>
<p><strong>Protocol:</strong> {{ protocol }}</p>
<p>
<strong>SYSSITE:</strong>
<span
@@ -35,6 +35,10 @@ export class LicensingComponent implements OnInit {
public activationKeyValue: string = ''
public applyingKeys: boolean = false
public protocol: string =
location.protocol === 'https:'
? 'HTTPS - secure connection'
: 'HTTP - insecure connection'
public syssite = this.appService.syssite
public currentLicenceKey = this.licenceService.licenceKey
@@ -6,4 +6,5 @@ export interface CellValidationSource {
extended_values?: string[]
hash: string
count: number
pending?: Promise<void>
}
@@ -160,14 +160,16 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
filters: true,
stretchH: 'all',
afterGetColHeader: baseAfterGetColHeader,
modifyColWidth: this.maxWidthCheker
modifyColWidth: this.maxWidthCheker,
theme: 'ht-theme-classic'
}
// Exclude data from settings for HOT v16 - it will be loaded manually
const { data, ...settingsWithoutData } = this.hotUserDatasets
this.hotUserDatasetsSettings = {
...settingsWithoutData,
licenseKey: this.hotTableLicenseKey
licenseKey: this.hotTableLicenseKey,
theme: 'ht-theme-classic'
}
}
+13 -3
View File
@@ -155,13 +155,23 @@ export class SasStoreService {
.adapterResponse
}
private libsPromise: Promise<any> | null = null
/**
*
* @returns All libraries
*/
public async viewLibs() {
return (await this.sasService.request('public/viewlibs', null))
.adapterResponse
public viewLibs() {
if (!this.libsPromise) {
this.libsPromise = this.sasService
.request('public/viewlibs', null)
.then((res: any) => res.adapterResponse)
.catch((err: any) => {
this.libsPromise = null
throw err
})
}
return this.libsPromise
}
public async refreshLibInfo(libref: string) {
@@ -0,0 +1,26 @@
<clr-modal
[clrModalOpen]="open"
[clrModalClosable]="false"
[clrModalStaticBackdrop]="true"
[clrModalSize]="'sm'"
>
<h3 class="modal-title">Validating cells</h3>
<div class="modal-body bulk-validation-body">
<div class="bulk-validation-row">
<clr-spinner clrSmall></clr-spinner>
<span class="bulk-validation-text">
Validating {{ done }} / {{ total }}…
</span>
</div>
<clr-progress-bar
class="bulk-validation-progress"
[clrValue]="done"
[clrMax]="total"
></clr-progress-bar>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">
Cancel
</button>
</div>
</clr-modal>
@@ -0,0 +1,19 @@
.bulk-validation-body {
display: flex;
flex-direction: column;
gap: 10px;
.bulk-validation-row {
display: flex;
align-items: center;
gap: 10px;
}
.bulk-validation-text {
flex: 1 1 auto;
}
.bulk-validation-progress {
margin: 0;
}
}
@@ -0,0 +1,19 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
@Component({
selector: 'app-bulk-validation-modal',
templateUrl: './bulk-validation-modal.component.html',
styleUrls: ['./bulk-validation-modal.component.scss'],
standalone: false
})
export class BulkValidationModalComponent {
@Input() open = false
@Input() done = 0
@Input() total = 0
@Output() cancel = new EventEmitter<void>()
onCancel() {
this.cancel.emit()
}
}
@@ -0,0 +1,18 @@
<clr-modal
[clrModalOpen]="open"
(clrModalOpenChange)="onClrModalOpenChange($event)"
[clrModalSize]="'md'"
>
<h3 class="modal-title">{{ title }}</h3>
<div class="modal-body">
<div>{{ message }}</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">
{{ cancelText }}
</button>
<button type="button" class="btn btn-primary" (click)="onConfirm()">
{{ confirmText }}
</button>
</div>
</clr-modal>
@@ -0,0 +1,31 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
@Component({
selector: 'app-confirm-modal',
templateUrl: './confirm-modal.component.html',
standalone: false
})
export class ConfirmModalComponent {
@Input() open = false
@Input() title = 'Confirm'
@Input() message = ''
@Input() confirmText = 'Yes'
@Input() cancelText = 'No'
@Output() result = new EventEmitter<boolean>()
onConfirm() {
this.result.emit(true)
}
onCancel() {
this.result.emit(false)
}
onClrModalOpenChange(value: boolean) {
// Close triggered by X / outside / Esc — treat as cancel. Only emit
// when modal was actually open (avoid double-emit when parent closes
// us via [open]=false in response to the Yes/No button).
if (!value && this.open) this.result.emit(false)
}
}
@@ -22,6 +22,7 @@ import { dqValidate } from './validations/dq-validation'
import { specialMissingNumericValidator } from './validations/hot-custom-validators'
import { applyNumericFormats } from './utils/applyNumericFormats'
import { CustomAutocompleteEditor } from './editors/numericAutocomplete'
import { makeNumberFormatRenderer } from '../../editor/utils/renderers.utils'
export class DcValidator {
private rules: DcValidation[] = []
@@ -147,6 +148,25 @@ export class DcValidator {
return getNotNullDefault(col, this.dqrules, colRule?.type)
}
/**
* Returns the num_digits for a ROUND rule on the given column, used to
* round edited/pasted values Excel-style. Returns undefined if no ROUND
* rule exists or its RULE_VALUE is not an integer.
*
* @param col column name
*/
getRoundDigits(col: string): number | undefined {
const roundRule = this.dqrules.find(
(rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'ROUND'
)
if (!roundRule) return undefined
const digits = parseInt(roundRule.RULE_VALUE, 10)
return isNaN(digits) ? undefined : digits
}
/**
* Retrieves dropdown source for given dc validation rule
* The values comes from MPE_SELECTBOX table
@@ -317,6 +337,30 @@ export class DcValidator {
if (this.hasDqRules(ruleColName, ['NOTNULL'])) {
this.rules[i].allowEmpty = false
}
// READONLY: render column read-only (default value handled via getColumnDefault on add row)
if (this.hasDqRules(ruleColName, ['READONLY'])) {
this.rules[i].readOnly = true
}
// HIDDEN: hide column in HOT but keep its data (still submitted via hot.getData())
if (this.hasDqRules(ruleColName, ['HIDDEN'])) {
this.hiddenColumns.push(i)
}
// NUMBER_FORMAT: display-only Intl.NumberFormat renderer.
// Set last so it overrides a dropdown's 'autocomplete' renderer (last-wins).
if (this.hasDqRules(ruleColName, ['NUMBER_FORMAT'])) {
const fmtRule = this.getDqDetails(ruleColName).find(
(rule: DQRule) => rule.RULE_TYPE === 'NUMBER_FORMAT'
)
this.rules[i].renderer = makeNumberFormatRenderer(fmtRule?.RULE_VALUE)
// Clear numbro's numericFormat (set by applyNumericFormats on every
// numeric column). On a numeric cell HOT lets numericFormat re-render
// via numbro, which overrides our Intl renderer and drops the currency
// symbol. The numeric editor/validator stay intact via `type`.
this.rules[i].numericFormat = undefined
}
}
// Correct format comes as STRING from SAS. That could be also fixed on SAS side.
@@ -14,3 +14,7 @@ export type DQRuleTypes =
| 'CASE'
| 'MINVAL'
| 'MAXVAL'
| 'READONLY'
| 'HIDDEN'
| 'ROUND'
| 'NUMBER_FORMAT'
@@ -235,6 +235,37 @@ describe('DC Validator', () => {
}
)
})
it('should apply READONLY, HIDDEN, NUMBER_FORMAT and ROUND rules', () => {
const dcValidator: DcValidator = new DcValidator(
example_sasparams,
example_dataformats,
example_cols,
example_dqRules,
example_dqData
)
// READONLY -> column rendered read-only
expect(dcValidator.getRule('SOME_BESTNUM')?.readOnly).toBeTrue()
// NUMBER_FORMAT -> a function renderer is assigned and numbro numericFormat
// is cleared so it can't override the Intl currency/percent rendering
expect(typeof dcValidator.getRule('SOME_BESTNUM')?.renderer).toEqual(
'function'
)
expect(dcValidator.getRule('SOME_BESTNUM')?.numericFormat).toBeUndefined()
// HIDDEN -> column index added to hidden columns
const rules = dcValidator.getRules()
const hiddenIndex = rules.findIndex(
(rule) => rule.data === 'PRIMARY_KEY_FIELD'
)
expect(dcValidator.getHiddenColumns()).toContain(hiddenIndex)
// ROUND -> num_digits returned for the column, undefined otherwise
expect(dcValidator.getRoundDigits('SOME_SHORTNUM')).toEqual(2)
expect(dcValidator.getRoundDigits('SOME_NUM')).toBeUndefined()
})
})
const example_dqData = [
@@ -313,6 +344,30 @@ const example_dqRules: any = [
RULE_TYPE: 'CASE',
RULE_VALUE: 'LOWCASE',
X: 0
},
{
BASE_COL: 'SOME_BESTNUM',
RULE_TYPE: 'READONLY',
RULE_VALUE: '7',
X: 0
},
{
BASE_COL: 'SOME_BESTNUM',
RULE_TYPE: 'NUMBER_FORMAT',
RULE_VALUE: '{"minimumFractionDigits":2}',
X: 0
},
{
BASE_COL: 'PRIMARY_KEY_FIELD',
RULE_TYPE: 'HIDDEN',
RULE_VALUE: '99',
X: 0
},
{
BASE_COL: 'SOME_SHORTNUM',
RULE_TYPE: 'ROUND',
RULE_VALUE: '2',
X: 0
}
]
@@ -0,0 +1,39 @@
import { excelRound } from '../utils/excelRound'
describe('DC Validator - excelRound', () => {
it('should round to the given number of decimal places', () => {
expect(excelRound(1.23456, 2)).toEqual(1.23)
expect(excelRound(1.236, 2)).toEqual(1.24)
expect(excelRound(2.5, 0)).toEqual(3)
})
it('should round half away from zero for negative values', () => {
expect(excelRound(-0.5, 0)).toEqual(-1)
expect(excelRound(-2.5, 0)).toEqual(-3)
expect(excelRound(-1.23456, 2)).toEqual(-1.23)
})
it('should support negative digits (round to tens/hundreds)', () => {
expect(excelRound(25, -1)).toEqual(30)
expect(excelRound(24, -1)).toEqual(20)
expect(excelRound(150, -2)).toEqual(200)
})
// Examples from Microsoft's ROUND documentation:
// https://support.microsoft.com/en-us/office/round-function-c018c5d8-40fb-4053-90b1-b3e7f61a213c
it('should match the Microsoft ROUND examples', () => {
expect(excelRound(2.15, 1)).toEqual(2.2)
expect(excelRound(2.149, 1)).toEqual(2.1)
expect(excelRound(-1.475, 2)).toEqual(-1.48)
expect(excelRound(21.5, -1)).toEqual(20)
expect(excelRound(626.3, -3)).toEqual(1000)
expect(excelRound(1.98, -1)).toEqual(0)
expect(excelRound(-50.55, -2)).toEqual(-100)
})
it('should return the original value when not finite', () => {
expect(excelRound(NaN, 2)).toBeNaN()
expect(excelRound(Infinity, 2)).toEqual(Infinity)
expect(excelRound(5, NaN)).toEqual(5)
})
})
@@ -0,0 +1,40 @@
import { DQRule } from '../models/dq-rules.model'
import { getColumnDefault } from '../utils/getColumnDefault'
describe('DC Validator - getColumnDefault', () => {
const dqRules: DQRule[] = [
{ BASE_COL: 'NOTNULL_COL', RULE_TYPE: 'NOTNULL', RULE_VALUE: 'nn', X: 1 },
{ BASE_COL: 'READONLY_COL', RULE_TYPE: 'READONLY', RULE_VALUE: 'ro', X: 1 },
{ BASE_COL: 'HIDDEN_COL', RULE_TYPE: 'HIDDEN', RULE_VALUE: 'hd', X: 1 },
{ BASE_COL: 'NUM_COL', RULE_TYPE: 'READONLY', RULE_VALUE: '42', X: 1 },
{ BASE_COL: 'EMPTY_COL', RULE_TYPE: 'HIDDEN', RULE_VALUE: ' ', X: 1 },
{ BASE_COL: 'OTHER_COL', RULE_TYPE: 'HARDSELECT', RULE_VALUE: 'x', X: 1 }
]
it('should return defaults for NOTNULL, READONLY and HIDDEN rules', () => {
expect(getColumnDefault('NOTNULL_COL', dqRules, 'text')).toEqual('nn')
expect(getColumnDefault('READONLY_COL', dqRules, 'text')).toEqual('ro')
expect(getColumnDefault('HIDDEN_COL', dqRules, 'text')).toEqual('hd')
})
it('should coerce to number for numeric columns', () => {
expect(getColumnDefault('NUM_COL', dqRules, 'numeric')).toEqual(42)
})
it('should return string for numeric columns when value is non-numeric', () => {
expect(getColumnDefault('READONLY_COL', dqRules, 'numeric')).toEqual('ro')
})
it('should ignore empty RULE_VALUE', () => {
expect(getColumnDefault('EMPTY_COL', dqRules, 'text')).toBeUndefined()
})
it('should return undefined for rules that do not provide defaults', () => {
expect(getColumnDefault('OTHER_COL', dqRules, 'text')).toBeUndefined()
})
it('should return undefined for non-existent columns and empty rules', () => {
expect(getColumnDefault('MISSING', dqRules, 'text')).toBeUndefined()
expect(getColumnDefault('NOTNULL_COL', [], 'text')).toBeUndefined()
})
})
@@ -0,0 +1,16 @@
/**
* Rounds a number like Excel's ROUND(number, num_digits):
* - rounds half away from zero (so ROUND(-0.5) === -1, ROUND(2.5) === 3)
* - supports negative `digits` (e.g. -1 rounds to the nearest ten)
*
* @param value number to round
* @param digits number of decimal places (may be negative)
* @returns the rounded number, or the original value if it is not finite
*/
export function excelRound(value: number, digits: number): number {
if (!isFinite(value) || !isFinite(digits)) return value
const factor = Math.pow(10, digits)
return (Math.sign(value) * Math.round(Math.abs(value) * factor)) / factor
}
@@ -0,0 +1,44 @@
import { DQRule, DQRuleTypes } from '../models/dq-rules.model'
/**
* Rule types whose RULE_VALUE supplies the default value inserted into a cell
* when a new row is added. Checked in priority order.
*/
const DEFAULT_VALUE_RULE_TYPES: DQRuleTypes[] = [
'NOTNULL',
'READONLY',
'HIDDEN'
]
/**
* Returns the default value for a column from DQ rules, used to pre-fill cells
* when a new row is added. Looks for the first non-empty RULE_VALUE among
* NOTNULL, READONLY and HIDDEN rules. Converts to number for numeric columns.
*
* @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 a default-providing rule exists
* with a non-empty value, otherwise undefined
*/
export function getColumnDefault(
colName: string,
dqRules: DQRule[],
colType?: string
): string | number | undefined {
const rule = dqRules.find(
(rule: DQRule) =>
rule.BASE_COL === colName &&
DEFAULT_VALUE_RULE_TYPES.includes(rule.RULE_TYPE) &&
rule.RULE_VALUE != null &&
rule.RULE_VALUE.trim().length > 0
)
if (!rule) return undefined
if (colType === 'numeric' && !isNaN(Number(rule.RULE_VALUE))) {
return Number(rule.RULE_VALUE)
}
return rule.RULE_VALUE
}
@@ -1,6 +1,6 @@
import { DcValidation } from '../models/dc-validation.model'
import { DQRule } from '../models/dq-rules.model'
import { getNotNullDefault } from './getNotNullDefault'
import { getColumnDefault } from './getColumnDefault'
const schemaTypeMap: { [key: string]: any } = {
numeric: '',
@@ -9,16 +9,16 @@ 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
* Priority: NOTNULL/READONLY/HIDDEN RULE_VALUE > autocomplete first option > type default
*/
export function getHotDataSchema(
type: string | undefined,
cellValidation?: DcValidation,
dqRules?: DQRule[]
): any {
// Check for NOTNULL default first
// Check for a rule-supplied default (NOTNULL/READONLY/HIDDEN) first
if (dqRules && cellValidation?.data) {
const defaultValue = getNotNullDefault(cellValidation.data, dqRules, type)
const defaultValue = getColumnDefault(cellValidation.data, dqRules, type)
if (defaultValue !== undefined) {
return defaultValue
}
+8 -2
View File
@@ -15,6 +15,8 @@ import { DirectivesModule } from '../directives/directives.module'
import { DatasetInfoComponent } from './dataset-info/dataset-info.component'
import { ContactLinkComponent } from './contact-link/contact-link.component'
import { ExcelPasswordModalComponent } from './excel-password-modal/excel-password-modal.component'
import { ConfirmModalComponent } from './confirm-modal/confirm-modal.component'
import { BulkValidationModalComponent } from './bulk-validation-modal/bulk-validation-modal.component'
@NgModule({
imports: [
@@ -32,7 +34,9 @@ import { ExcelPasswordModalComponent } from './excel-password-modal/excel-passwo
TermsComponent,
DatasetInfoComponent,
ContactLinkComponent,
ExcelPasswordModalComponent
ExcelPasswordModalComponent,
ConfirmModalComponent,
BulkValidationModalComponent
],
exports: [
LoadingIndicatorComponent,
@@ -42,7 +46,9 @@ import { ExcelPasswordModalComponent } from './excel-password-modal/excel-passwo
TermsComponent,
DatasetInfoComponent,
ContactLinkComponent,
ExcelPasswordModalComponent
ExcelPasswordModalComponent,
ConfirmModalComponent,
BulkValidationModalComponent
],
providers: [UserService, AlertsService]
})
@@ -544,7 +544,8 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
maxRows: viewboxTable.hotTable.maxRows || Infinity,
manualColumnResize: true,
rowHeaders: true,
licenseKey: viewboxTable.hotTable.licenseKey
licenseKey: viewboxTable.hotTable.licenseKey,
theme: 'ht-theme-classic'
}
// Force a new object reference to trigger change detection
+2 -1
View File
@@ -75,7 +75,8 @@ export class StageComponent implements OnInit, AfterViewInit {
afterInit: this.hotTable.afterInit,
stretchH: 'all',
cells: this.hotTable.cells,
className: 'htDark'
className: 'htDark',
theme: 'ht-theme-classic'
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ const routes: Routes = [{ path: ':tableId', component: StageComponent }]
CommonModule,
ClarityModule,
RouterModule.forChild(routes),
HotTableModule.forRoot()
HotTableModule
]
})
export class StageModule {}
+50 -26
View File
@@ -33,6 +33,7 @@ import {
Version
} from '../models/sas/editors-getdata.model'
import { mergeColsRules } from '../shared/dc-validator/utils/mergeColsRules'
import { makeNumberFormatRenderer } from '../editor/utils/renderers.utils'
import { PublicViewtablesServiceResponse } from '../models/sas/public-viewtables.model'
import { PublicViewlibsServiceResponse } from '../models/sas/public-viewlibs.model'
import { PublicRefreshlibinfoServiceResponse } from '../models/sas/public-refreshlibinfo.model'
@@ -122,6 +123,7 @@ export class ViewerComponent
stretchH: 'all',
modifyColWidth: this.maxWidthCheker,
cells: this.hotTable.cells,
hiddenColumns: { columns: this.hiddenViewColumns, indicators: false },
maxRows: this.hotTable.maxRows,
manualColumnResize: true,
afterGetColHeader: this.hotTable.afterGetColHeader,
@@ -129,12 +131,15 @@ export class ViewerComponent
rowHeaderWidth: this.hotTable.rowHeaderWidth,
rowHeights: this.hotTable.rowHeights,
licenseKey: this.hotTable.licenseKey,
className: 'htDark'
className: 'htDark',
theme: 'ht-theme-classic'
}
}
public numberOfRows: number | null = null
public headerPks: string[] = []
public $dataFormats: $DataFormats | null = null
// Physical column indexes to hide in the viewer (from HIDDEN DQ rules)
public hiddenViewColumns: number[] = []
public datasetInfo: boolean = false
public dsmeta: DSMeta[] = []
public versions: Version[] = []
@@ -769,18 +774,14 @@ export class ViewerComponent
let ds = []
ds = this.libDataset.split('.')
if (globals.viewer.startupSet) {
this.libraries = globals.viewer.libraries
} else {
await this.sasStoreService
.viewLibs()
.then((res: any) => {
this.libraries = res.saslibs
})
.catch((err: any) => {
this.loggerService.error(err)
})
}
await this.sasStoreService
.viewLibs()
.then((res: any) => {
this.libraries = res.saslibs
})
.catch((err: any) => {
this.loggerService.error(err)
})
this.lib = ds[0]
@@ -815,18 +816,14 @@ export class ViewerComponent
libDataset = this.libDataset
this.libTab = libDataset
} else {
if (globals.viewer.startupSet) {
this.libraries = globals.viewer.libraries
} else {
await this.sasStoreService
.viewLibs()
.then((res: any) => {
this.libraries = res.saslibs
})
.catch((err: any) => {
this.loggerService.error(err)
})
}
await this.sasStoreService
.viewLibs()
.then((res: any) => {
this.libraries = res.saslibs
})
.catch((err: any) => {
this.loggerService.error(err)
})
if (typeof this.table !== 'undefined') {
if (globals.viewer.startupSet) {
@@ -911,16 +908,43 @@ export class ViewerComponent
}
}
// NUMBER_FORMAT (display) and HIDDEN (visibility) are the DQ rules
// that make sense in the read-only viewer; READONLY/ROUND are
// editor-only. Build lookups for both from the response.
const numberFormats: { [col: string]: string } = {}
const hiddenCols = new Set<string>()
if (Array.isArray(res.dqrules)) {
for (const rule of res.dqrules) {
if (rule.RULE_TYPE === 'NUMBER_FORMAT') {
numberFormats[rule.BASE_COL] = rule.RULE_VALUE
}
if (rule.RULE_TYPE === 'HIDDEN') {
hiddenCols.add(rule.BASE_COL)
}
}
}
const hiddenColumnIndexes: number[] = []
for (let index = 0; index < colArr.length; index++) {
columns.push({ data: colArr[index] })
const col = colArr[index]
const colDef: any = { data: col }
if (numberFormats[col]) {
colDef.renderer = makeNumberFormatRenderer(numberFormats[col])
}
if (hiddenCols.has(col)) {
hiddenColumnIndexes.push(index)
}
columns.push(colDef)
}
this.hotTable.colHeaders = colArr
this.hotTable.columns = columns
this.hiddenViewColumns = hiddenColumnIndexes
} else {
// Set empty arrays if no data
this.hotTable.colHeaders = []
this.hotTable.columns = []
this.hiddenViewColumns = []
}
// Set cells function
+2 -1
View File
@@ -162,7 +162,8 @@ export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
rowHeaderWidth: 15,
rowHeights: 20,
licenseKey: this.hotTableLicenseKey,
className: 'htDark'
className: 'htDark',
theme: 'ht-theme-classic'
}
}
@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><g opacity='0.6'><path d='M11.5859 6L8.29304 9.29289L5.00015 6' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></g></svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11.0713 4.64188L7.72115 7.99203L11.0713 11.3422M4.92936 4.08353L4.92936 11.3422' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>

After

Width:  |  Height:  |  Size: 262 B

@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M9.49268 11.2929L6.19978 8.00001L9.49268 4.70712' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>

After

Width:  |  Height:  |  Size: 231 B

@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M8.00004 3.33331V12.6666M8.00004 12.6666L10.6667 9.99998M8.00004 12.6666L5.33337 9.99998' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>

After

Width:  |  Height:  |  Size: 271 B

@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M8.00008 12.6667L8.00008 3.33335M8.00008 3.33335L5.33342 6.00002M8.00008 3.33335L10.6667 6.00002' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>

After

Width:  |  Height:  |  Size: 279 B

@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M4.9292 4.64188L8.27934 7.99203L4.9292 11.3422M11.0711 4.08353V11.3422' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>

After

Width:  |  Height:  |  Size: 252 B

@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M6.64648 10.9393L9.93938 7.64644L6.64648 4.35354' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>

After

Width:  |  Height:  |  Size: 231 B

@@ -0,0 +1 @@
<svg width='8' height='8' viewBox='0 0 8 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M5.9999 3C6.2839 3 6.43224 3.32867 6.2609 3.541L6.23557 3.569L4.23557 5.569C4.17817 5.62639 4.10181 5.66087 4.0208 5.66596C3.93979 5.67106 3.8597 5.64642 3.79557 5.59667L3.76424 5.569L1.76424 3.569L1.73657 3.53767L1.71857 3.512L1.70057 3.48L1.6949 3.468L1.6859 3.44567L1.67524 3.40967L1.6719 3.392L1.66857 3.372L1.66724 3.353V3.31367L1.6689 3.29433L1.6719 3.27433L1.67524 3.257L1.6859 3.221L1.6949 3.19867L1.71824 3.15467L1.7399 3.12467L1.76424 3.09767L1.79557 3.07L1.82124 3.052L1.85324 3.034L1.86524 3.02833L1.88757 3.01933L1.92357 3.00867L1.94124 3.00533L1.96124 3.002L1.98024 3.00067L5.9999 3Z' fill='currentColor'/></svg>

After

Width:  |  Height:  |  Size: 727 B

@@ -0,0 +1 @@
<svg width='10' height='10' viewBox='0 0 10 10' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M6.08482 1.35771L6.11503 1.3551H6.17649L6.2067 1.35771L6.23795 1.36239L6.26503 1.3676L6.32128 1.38427L6.35618 1.39833L6.42493 1.43479L6.4718 1.46864L6.51399 1.50667L6.55722 1.55562L6.58534 1.59573L6.61347 1.64573L6.62232 1.66448L6.63639 1.69937L6.65305 1.75562L6.65826 1.78323L6.66347 1.81448L6.66555 1.84417L6.66659 1.87489V8.12489C6.66659 8.56864 6.15305 8.80042 5.82128 8.53271L5.77753 8.49312L2.65253 5.36812C2.56286 5.27844 2.50899 5.15912 2.50103 5.03254C2.49307 4.90596 2.53157 4.78083 2.6093 4.68062L2.65253 4.63167L5.77753 1.50667L5.82649 1.46344L5.86659 1.43531L5.91659 1.40719L5.93534 1.39833L5.97024 1.38427L6.02649 1.3676L6.05409 1.36239L6.08482 1.35771Z' fill='currentColor'/></svg>

After

Width:  |  Height:  |  Size: 800 B

@@ -0,0 +1 @@
<svg width='10' height='10' viewBox='0 0 10 10' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M3.33337 1.87499C3.33337 1.43124 3.84692 1.19947 4.17869 1.46718L4.22244 1.50676L7.34744 4.63176C7.43711 4.72145 7.49098 4.84077 7.49894 4.96735C7.5069 5.09392 7.4684 5.21905 7.39067 5.31926L7.34744 5.36822L4.22244 8.49322L4.17348 8.53645L4.13337 8.56457L4.08337 8.5927L4.06462 8.60155L4.02973 8.61562L3.97348 8.63228L3.94587 8.63749L3.91462 8.6427L3.88494 8.64478L3.85421 8.64582L3.82348 8.64478L3.79327 8.64218L3.76202 8.63749L3.73494 8.63228L3.67869 8.61562L3.64379 8.60155L3.57504 8.5651L3.52817 8.53124L3.48598 8.49322L3.44275 8.44426L3.41462 8.40416L3.3865 8.35416L3.37764 8.33541L3.36358 8.30051L3.34692 8.24426L3.34171 8.21666L3.3365 8.18541L3.33442 8.15572L3.33337 1.87499Z' fill='currentColor'/></svg>

After

Width:  |  Height:  |  Size: 815 B

@@ -0,0 +1 @@
<svg width='8' height='8' viewBox='0 0 8 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M3.76425 2.43099C3.82165 2.3736 3.89801 2.33913 3.97902 2.33403C4.06003 2.32894 4.14012 2.35358 4.20425 2.40333L4.23558 2.43099L6.23558 4.43099L6.26325 4.46233L6.28125 4.48799L6.29925 4.51999L6.30492 4.53199L6.31392 4.55433L6.32458 4.59033L6.32792 4.60799L6.33125 4.62799L6.33258 4.64699L6.33325 4.66666L6.33258 4.68633L6.33092 4.70566L6.32792 4.72566L6.32458 4.74299L6.31392 4.77899L6.30492 4.80133L6.28158 4.84533L6.25992 4.87533L6.23558 4.90233L6.20425 4.92999L6.17858 4.94799L6.14658 4.96599L6.13458 4.97166L6.11225 4.98066L6.07625 4.99133L6.05858 4.99466L6.03858 4.99799L6.01958 4.99933L5.99992 4.99999H1.99992C1.71592 4.99999 1.56758 4.67133 1.73892 4.45899L1.76425 4.43099L3.76425 2.43099Z' fill='currentColor'/></svg>

After

Width:  |  Height:  |  Size: 826 B

+1
View File
@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M3.33337 8.00002L6.66671 11.3334L13.3334 4.66669' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>

After

Width:  |  Height:  |  Size: 231 B

+1
View File
@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M5 8L7 10L11 6' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>

After

Width:  |  Height:  |  Size: 197 B

@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M12 4L4 12M4 4L12 12' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>

After

Width:  |  Height:  |  Size: 203 B

@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M4.33325 8H11.6666' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>

After

Width:  |  Height:  |  Size: 201 B

@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M7.49988 11.6667C7.49988 11.9428 7.72374 12.1667 7.99988 12.1667C8.27602 12.1667 8.49988 11.9428 8.49988 11.6667V8.50002H11.6666C11.9427 8.50002 12.1666 8.27616 12.1666 8.00002C12.1666 7.72388 11.9427 7.50002 11.6666 7.50002H8.49988V4.33337C8.49988 4.05723 8.27602 3.83337 7.99988 3.83337C7.72374 3.83337 7.49988 4.05723 7.49988 4.33337V7.50002H4.33325C4.05711 7.50002 3.83325 7.72388 3.83325 8.00002C3.83325 8.27616 4.05711 8.50002 4.33325 8.50002H7.49988V11.6667Z' fill='currentColor'/></svg>

After

Width:  |  Height:  |  Size: 639 B

+1
View File
@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11.0002 6.66663C11.4262 6.66663 11.6487 7.15963 11.3917 7.47813L11.3537 7.52013L8.35372 10.5201C8.26762 10.6062 8.15307 10.6579 8.03156 10.6656C7.91005 10.6732 7.78992 10.6363 7.69372 10.5616L7.64672 10.5201L4.64672 7.52013L4.60522 7.47313L4.57822 7.43463L4.55122 7.38663L4.54272 7.36863L4.52922 7.33513L4.51322 7.28113L4.50822 7.25463L4.50322 7.22463L4.50122 7.19613V7.13713L4.50372 7.10813L4.50822 7.07813L4.51322 7.05213L4.52922 6.99813L4.54272 6.96463L4.57772 6.89863L4.61022 6.85363L4.64672 6.81313L4.69372 6.77163L4.73222 6.74463L4.78022 6.71763L4.79822 6.70913L4.83172 6.69563L4.88572 6.67963L4.91222 6.67463L4.94222 6.66963L4.97072 6.66763L11.0002 6.66663Z' fill='currentColor'/></svg>

After

Width:  |  Height:  |  Size: 799 B

+1
View File
@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><circle cx='8' cy='8' r='4' fill='currentColor'/></svg>

After

Width:  |  Height:  |  Size: 151 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32' fill='none'><g opacity='0.5'><path d='M28 28L20 20M4 13.3333C4 14.559 4.24141 15.7727 4.71046 16.905C5.1795 18.0374 5.86699 19.0663 6.73367 19.933C7.60035 20.7997 8.62925 21.4872 9.76162 21.9562C10.894 22.4253 12.1077 22.6667 13.3333 22.6667C14.559 22.6667 15.7727 22.4253 16.905 21.9562C18.0374 21.4872 19.0663 20.7997 19.933 19.933C20.7997 19.0663 21.4872 18.0374 21.9562 16.905C22.4253 15.7727 22.6667 14.559 22.6667 13.3333C22.6667 12.1077 22.4253 10.894 21.9562 9.76162C21.4872 8.62925 20.7997 7.60035 19.933 6.73367C19.0663 5.86699 18.0374 5.1795 16.905 4.71046C15.7727 4.24141 14.559 4 13.3333 4C12.1077 4 10.894 4.24141 9.76162 4.71046C8.62925 5.1795 7.60035 5.86699 6.73367 6.73367C5.86699 7.60035 5.1795 8.62925 4.71046 9.76162C4.24141 10.894 4 12.1077 4 13.3333Z' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/></g></svg>

After

Width:  |  Height:  |  Size: 948 B

@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11.0002 6.66663C11.4262 6.66663 11.6487 7.15963 11.3917 7.47813L11.3537 7.52013L8.35372 10.5201C8.26762 10.6062 8.15307 10.6579 8.03156 10.6656C7.91005 10.6732 7.78992 10.6363 7.69372 10.5616L7.64672 10.5201L4.64672 7.52013L4.60522 7.47313L4.57822 7.43463L4.55122 7.38663L4.54272 7.36863L4.52922 7.33513L4.51322 7.28113L4.50822 7.25463L4.50322 7.22463L4.50122 7.19613V7.13713L4.50372 7.10813L4.50822 7.07813L4.51322 7.05213L4.52922 6.99813L4.54272 6.96463L4.57772 6.89863L4.61022 6.85363L4.64672 6.81313L4.69372 6.77163L4.73222 6.74463L4.78022 6.71763L4.79822 6.70913L4.83172 6.69563L4.88572 6.67963L4.91222 6.67463L4.94222 6.66963L4.97072 6.66763L11.0002 6.66663Z' fill='currentColor'/></svg>

After

Width:  |  Height:  |  Size: 799 B

+56 -1
View File
@@ -1,5 +1,8 @@
/* You can add global styles to this file, and also import other style files */
@import '~handsontable/dist/handsontable.full.css';
@import '~handsontable/styles/handsontable.min.css';
@import '~handsontable/styles/handsontable.min.css';
@import '~handsontable/styles/ht-theme-classic-no-icons.min.css';
@import './_hot-icons.scss';
@import '~@clr/icons/clr-icons.min.css';
@@ -539,6 +542,11 @@ body[cds-theme="dark"] {
color: #fff;
}
.modal-title-wrapper:focus,
.modal-title-wrapper:focus-visible {
outline: none;
}
body[cds-theme="dark"] {
.btn.btn-icon.btn-dimmed {
@@ -627,6 +635,41 @@ body[cds-theme="dark"] {
border-color: $darkBorderColor;
}
.handsontable .htDimmed {
color: #eee !important;
background-color: #3c5662 !important;
}
// Handsontable v17 themes (scrollbars, autocomplete listbox,
// dropdown/context menus, date-picker calendar, cell editor) solely through
// its --ht-* CSS variables, which the ht-theme-classic class resolves to
// light values via light-dark(). The hand-rolled .htDark/.darkTH marker
// classes can't reach those, so override the variables directly (the
// official theme-customization path). Scoped to [class*='ht-theme-classic']
// so it also covers the menu/listbox/calendar portals HT appends to <body>.
[class*='ht-theme-classic'] {
color-scheme: dark;
// Root vars — most derives from these
--ht-foreground-color: #eee;
--ht-background-color: #3c5662; // cells, menus, listbox, calendar bg
--ht-background-secondary-color: #2d4048; // scrollbar track, icon buttons
--ht-border-color: #{$darkBorderColor};
--ht-read-only-color: #eee; // read-only/dimmed text
--ht-header-background-color: #487d96;
--ht-header-row-background-color: #487d96;
--ht-header-highlighted-background-color: #3b6b81;
--ht-header-row-highlighted-background-color: #3b6b81;
// Scrollbar thumb + cell editor input
--ht-scrollbar-thumb-color: #{$darkBorderColor};
--ht-input-background-color: #708b98;
--ht-input-foreground-color: #fff;
--ht-cell-editor-background-color: #708b98;
--ht-cell-editor-foreground-color: #fff;
}
.handsontable tr:first-child th, .handsontable tr:first-child td {
border-color: $darkBorderColor;
}
@@ -4784,6 +4827,18 @@ body[cds-theme="dark"] {
.handsontable.listbox {
box-shadow: 0px 4px 20px 0px #00000070;
// HT v17 sizes the inner table to the dropdown width but the 1px L+R menu
// border eats 2px of that, leaving content 2px too wide -> a spurious
// horizontal scrollbar. Render the border as an outline (no layout/overflow
// impact) so the content fits exactly. outline-offset pulls it inside.
border: 0 !important;
outline: var(--ht-menu-border-width, 1px) solid var(--ht-menu-border-color, #e5e5e9);
outline-offset: calc(-1 * var(--ht-menu-border-width, 1px));
.wtHolder {
overflow-x: hidden;
}
}
.handsontable td.htInvalid {
+330 -517
View File
@@ -1,521 +1,334 @@
_webout=`{"SYSDATE" : "26SEP22"
,"SYSTIME" : "08:30"
, "approvers":
[
{
"PERSONNAME": "sasdemo",
"EMAIL": "sasdemo",
"USERID": "sasdemo"
function makeRows(n) {
const dropdowns = ["Option 1", "Option 2", "Option 3"]
const hardselects = ["Alpha", "Bravo", "Charlie"]
// SOME_NUM is a HARDSELECT_HOOK column — values must come from the dynamic
// validation service (editors/getdynamiccolvals.js -> `other`). Use a subset
// of those valid values so the cells pass validation.
const someNumValues = [
0.00105564761956, 0.00521895988156, 0.0058409725343, 0.00613050395908,
0.01305025071513, 0.01442142483518, 0.02422838845487, 0.0256445333481,
0.02624525922641, 0.02891322459509, 0.02972866503043, 0.03103561933666,
0.03150132113671, 0.03522355064527, 0.03529406247441, 0.03975785711768,
0.04319973664507, 0.04521169189606, 0.04787342951068, 0.04948144222119
]
const rows = []
for (let i = 0; i < n; i++) {
rows.push({
_____DELETE__THIS__RECORD_____: "No",
PRIMARY_KEY_FIELD: i,
SOME_CHAR: "dummy data row " + i,
SOME_DROPDOWN: dropdowns[i % dropdowns.length],
SOME_HARDSELECT: hardselects[i % hardselects.length],
SOME_NUM: someNumValues[i % someNumValues.length],
SOME_DATE: "1960-02-12",
SOME_DATETIME: "1960-01-01 00:00:42",
SOME_TIME: "00:00:42",
SOME_SHORTNUM: (i % 99) + 1,
SOME_BESTNUM: (i % 90) + 10,
// demo columns for the newer DQ rule types
READONLY_COL: "Readonly default",
HIDDEN_COL: "Hidden default",
ROUND_COL: Number((i + 1 + i / 7).toFixed(5)), // rounds on edit
NUMFMT_COL: 1000 + i * 12.5 // shown as EUR
})
}
return rows
}
]
, "cols":
[
{
"NAME": "PRIMARY_KEY_FIELD",
"VARNUM": 1,
"LABEL": "PRIMARY_KEY_FIELD",
"FMTNAME": "",
"DDTYPE": "NUMERIC",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_BESTNUM",
"VARNUM": 9,
"LABEL": "SOME_BESTNUM",
"FMTNAME": "BEST",
"DDTYPE": "NUMERIC",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_CHAR",
"VARNUM": 2,
"LABEL": "SOME_CHAR",
"FMTNAME": "",
"DDTYPE": "CHARACTER",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_DATE",
"VARNUM": 5,
"LABEL": "SOME_DATE",
"FMTNAME": "DATE",
"DDTYPE": "DATE",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_DATETIME",
"VARNUM": 6,
"LABEL": "SOME_DATETIME",
"FMTNAME": "DATETIME",
"DDTYPE": "DATETIME",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_DROPDOWN",
"VARNUM": 3,
"LABEL": "SOME_DROPDOWN",
"FMTNAME": "",
"DDTYPE": "CHARACTER",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_NUM",
"VARNUM": 4,
"LABEL": "SOME_NUM",
"FMTNAME": "",
"DDTYPE": "NUMERIC",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_SHORTNUM",
"VARNUM": 8,
"LABEL": "SOME_SHORTNUM",
"FMTNAME": "",
"DDTYPE": "NUMERIC",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_TIME",
"VARNUM": 7,
"LABEL": "SOME_TIME",
"FMTNAME": "TIME",
"DDTYPE": "TIME",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
}
]
, "dqdata":
[
{
"BASE_COL": "SOME_DROPDOWN",
"RULE_VALUE": "SOME_DROPDOWN",
"RULE_DATA": "Option 1",
"SELECTBOX_ORDER": 1
},
{
"BASE_COL": "SOME_DROPDOWN",
"RULE_VALUE": "SOME_DROPDOWN",
"RULE_DATA": "Option 2",
"SELECTBOX_ORDER": 2
},
{
"BASE_COL": "SOME_DROPDOWN",
"RULE_VALUE": "SOME_DROPDOWN",
"RULE_DATA": "Option 3",
"SELECTBOX_ORDER": 2
},
{
"BASE_COL": "SOME_DROPDOWN",
"RULE_VALUE": "SOME_DROPDOWN",
"RULE_DATA": "This is a long option. This option is very long. It is optional, though.",
"SELECTBOX_ORDER": 3
}
]
, "dqrules":
[
{
"BASE_COL": "PRIMARY_KEY_FIELD",
"RULE_TYPE": "NOTNULL",
"RULE_VALUE": "",
"X": 0
},
{
"BASE_COL": "SOME_NUM",
"RULE_TYPE": "HARDSELECT_HOOK",
"RULE_VALUE": "services/validations/mpe_x_test.some_num",
"X": 0
}
]
, "dsmeta":
[
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Data Set Name",
"VALUE": "DC996664.MPE_X_TEST"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Observations",
"VALUE": "496"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Member Type",
"VALUE": "DATA"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Variables",
"VALUE": "9"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Engine",
"VALUE": "V9"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Indexes",
"VALUE": "1"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Created",
"VALUE": "09/26/2022 08:24:39"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Integrity Constraints",
"VALUE": "1"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Last Modified",
"VALUE": "09/26/2022 08:24:45"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Observation Length",
"VALUE": "32947"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Protection",
"VALUE": " ."
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Deleted Observations",
"VALUE": "0"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Data Set Type",
"VALUE": " ."
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Compressed",
"VALUE": "CHAR"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Label",
"VALUE": " ."
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Reuse Space",
"VALUE": "NO"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Data Representation",
"VALUE": "WINDOWS_64"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Point to Observations",
"VALUE": "YES"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Encoding",
"VALUE": "wlatin1 Western (Windows)"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Sorted",
"VALUE": "NO"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Data Set Page Size",
"VALUE": "262144"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Number of Data Set Pages",
"VALUE": "3"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Index File Page Size",
"VALUE": "4096"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Number of Index File Pages",
"VALUE": "4"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Number of Data Set Repairs",
"VALUE": "0"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "ExtendObsCounter",
"VALUE": "YES"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Filename",
"VALUE": "C:\DataController\DC996664\mpe_x_test.sas7bdat"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Release Created",
"VALUE": "9.0401M7"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Host Created",
"VALUE": "X64_DSRV16"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Owner Name",
"VALUE": "BUILTIN\Administrators"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "File Size",
"VALUE": " 1MB"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "File Size (bytes)",
"VALUE": "1048576"
}
]
, "maxvarlengths":
[
{
"NAME": "_____DELETE__THIS__RECORD_____",
"MAXLEN": 3
},
{
"NAME": "PRIMARY_KEY_FIELD",
"MAXLEN": 4
},
{
"NAME": "some_char",
"MAXLEN": 591
},
{
"NAME": "some_dropdown",
"MAXLEN": 8
},
{
"NAME": "some_num",
"MAXLEN": 8
},
{
"NAME": "some_date",
"MAXLEN": 10
},
{
"NAME": "some_datetime",
"MAXLEN": 19
},
{
"NAME": "some_time",
"MAXLEN": 8
},
{
"NAME": "some_shortnum",
"MAXLEN": 3
},
{
"NAME": "some_bestnum",
"MAXLEN": 3
}
]
, "query":
[
]
, "sasdata":
[
{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":0 ,"SOME_CHAR":"this is dummy data" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":42 ,"SOME_DATE":"1960-02-12" ,"SOME_DATETIME":"1960-01-01 00:00:42" ,"SOME_TIME":"00:00:42" ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1 ,"SOME_CHAR":"more dummy data" ,"SOME_DROPDOWN":"Option 2" ,"SOME_NUM":42 ,"SOME_DATE":"1960-02-12" ,"SOME_DATETIME":"1960-01-01 00:00:42" ,"SOME_TIME":"00:07:02" ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":2 ,"SOME_CHAR":"even more dummy data" ,"SOME_DROPDOWN":"Option 3" ,"SOME_NUM":42 ,"SOME_DATE":"1960-02-12" ,"SOME_DATETIME":"1960-01-01 00:00:42" ,"SOME_TIME":"00:02:22" ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":3 ,"SOME_CHAR":"It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:" ,"SOME_DROPDOWN":"Option 2" ,"SOME_NUM":1613.001 ,"SOME_DATE":"1961-02-27" ,"SOME_DATETIME":"1960-01-01 00:07:03" ,"SOME_TIME":"00:00:44" ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":4 ,"SOME_CHAR":"if you can fill the unforgiving minute" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":1613.0011235 ,"SOME_DATE":"1971-08-02" ,"SOME_DATETIME":"1973-05-29 06:12:03" ,"SOME_TIME":"00:06:52" ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1010 ,"SOME_CHAR":"10 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.3677867113 ,"SOME_DATE":"1961-03-05" ,"SOME_DATETIME":"1960-01-01 08:16:44" ,"SOME_TIME":"00:00:35" ,"SOME_SHORTNUM":1 ,"SOME_BESTNUM":72 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1011 ,"SOME_CHAR":"11 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8693330497 ,"SOME_DATE":"1961-01-20" ,"SOME_DATETIME":"1960-01-01 01:25:19" ,"SOME_TIME":"00:00:01" ,"SOME_SHORTNUM":6 ,"SOME_BESTNUM":54 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1012 ,"SOME_CHAR":"12 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5432779065 ,"SOME_DATE":"1961-10-06" ,"SOME_DATETIME":"1960-01-01 02:57:35" ,"SOME_TIME":"00:00:35" ,"SOME_SHORTNUM":54 ,"SOME_BESTNUM":62 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1013 ,"SOME_CHAR":"13 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5051939867 ,"SOME_DATE":"1962-02-20" ,"SOME_DATETIME":"1960-01-01 06:47:55" ,"SOME_TIME":"00:00:41" ,"SOME_SHORTNUM":38 ,"SOME_BESTNUM":4 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1014 ,"SOME_CHAR":"14 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0130502507 ,"SOME_DATE":"1960-01-13" ,"SOME_DATETIME":"1960-01-01 03:48:13" ,"SOME_TIME":"00:00:14" ,"SOME_SHORTNUM":92 ,"SOME_BESTNUM":57 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1015 ,"SOME_CHAR":"15 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5822708009 ,"SOME_DATE":"1962-07-12" ,"SOME_DATETIME":"1960-01-01 12:05:18" ,"SOME_TIME":"00:00:54" ,"SOME_SHORTNUM":92 ,"SOME_BESTNUM":80 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1016 ,"SOME_CHAR":"16 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1382724979 ,"SOME_DATE":"1960-08-29" ,"SOME_DATETIME":"1960-01-01 02:48:01" ,"SOME_TIME":"00:00:01" ,"SOME_SHORTNUM":28 ,"SOME_BESTNUM":91 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1017 ,"SOME_CHAR":"17 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.892701324 ,"SOME_DATE":"1961-09-14" ,"SOME_DATETIME":"1960-01-01 07:03:58" ,"SOME_TIME":"00:01:37" ,"SOME_SHORTNUM":91 ,"SOME_BESTNUM":72 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1018 ,"SOME_CHAR":"18 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1852788567 ,"SOME_DATE":"1961-03-08" ,"SOME_DATETIME":"1960-01-01 00:22:48" ,"SOME_TIME":"00:00:32" ,"SOME_SHORTNUM":93 ,"SOME_BESTNUM":79 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1019 ,"SOME_CHAR":"19 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0737551018 ,"SOME_DATE":"1961-01-24" ,"SOME_DATETIME":"1960-01-01 03:14:33" ,"SOME_TIME":"00:00:21" ,"SOME_SHORTNUM":22 ,"SOME_BESTNUM":90 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1020 ,"SOME_CHAR":"20 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7128569939 ,"SOME_DATE":"1961-02-08" ,"SOME_DATETIME":"1960-01-01 01:50:23" ,"SOME_TIME":"00:01:40" ,"SOME_SHORTNUM":65 ,"SOME_BESTNUM":34 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1021 ,"SOME_CHAR":"21 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6706138443 ,"SOME_DATE":"1961-03-09" ,"SOME_DATETIME":"1960-01-01 04:52:55" ,"SOME_TIME":"00:00:13" ,"SOME_SHORTNUM":44 ,"SOME_BESTNUM":97 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1022 ,"SOME_CHAR":"22 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1423215792 ,"SOME_DATE":"1962-07-22" ,"SOME_DATETIME":"1960-01-01 07:25:01" ,"SOME_TIME":"00:01:10" ,"SOME_SHORTNUM":66 ,"SOME_BESTNUM":98 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1023 ,"SOME_CHAR":"23 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1259848066 ,"SOME_DATE":"1962-09-01" ,"SOME_DATETIME":"1960-01-01 09:32:34" ,"SOME_TIME":"00:01:16" ,"SOME_SHORTNUM":44 ,"SOME_BESTNUM":98 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1024 ,"SOME_CHAR":"24 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.3899468637 ,"SOME_DATE":"1961-12-06" ,"SOME_DATETIME":"1960-01-01 06:53:51" ,"SOME_TIME":"00:00:33" ,"SOME_SHORTNUM":30 ,"SOME_BESTNUM":90 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1025 ,"SOME_CHAR":"25 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0310356193 ,"SOME_DATE":"1960-03-01" ,"SOME_DATETIME":"1960-01-01 02:58:07" ,"SOME_TIME":"00:00:27" ,"SOME_SHORTNUM":73 ,"SOME_BESTNUM":59 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1026 ,"SOME_CHAR":"26 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9057884239 ,"SOME_DATE":"1960-10-04" ,"SOME_DATETIME":"1960-01-01 11:17:28" ,"SOME_TIME":"00:00:41" ,"SOME_SHORTNUM":82 ,"SOME_BESTNUM":46 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1027 ,"SOME_CHAR":"27 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5920675856 ,"SOME_DATE":"1962-07-15" ,"SOME_DATETIME":"1960-01-01 03:35:41" ,"SOME_TIME":"00:00:22" ,"SOME_SHORTNUM":46 ,"SOME_BESTNUM":73 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1028 ,"SOME_CHAR":"28 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6580030046 ,"SOME_DATE":"1960-10-08" ,"SOME_DATETIME":"1960-01-01 13:13:30" ,"SOME_TIME":"00:00:40" ,"SOME_SHORTNUM":35 ,"SOME_BESTNUM":40 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1029 ,"SOME_CHAR":"29 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.807042594 ,"SOME_DATE":"1960-12-26" ,"SOME_DATETIME":"1960-01-01 11:57:14" ,"SOME_TIME":"00:00:19" ,"SOME_SHORTNUM":80 ,"SOME_BESTNUM":12 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1030 ,"SOME_CHAR":"30 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8801450408 ,"SOME_DATE":"1961-05-15" ,"SOME_DATETIME":"1960-01-01 10:11:05" ,"SOME_TIME":"00:00:25" ,"SOME_SHORTNUM":70 ,"SOME_BESTNUM":19 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1031 ,"SOME_CHAR":"31 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4150194705 ,"SOME_DATE":"1962-01-27" ,"SOME_DATETIME":"1960-01-01 11:27:09" ,"SOME_TIME":"00:01:04" ,"SOME_SHORTNUM":94 ,"SOME_BESTNUM":48 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1032 ,"SOME_CHAR":"32 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9743401203 ,"SOME_DATE":"1962-01-09" ,"SOME_DATETIME":"1960-01-01 07:44:35" ,"SOME_TIME":"00:01:07" ,"SOME_SHORTNUM":43 ,"SOME_BESTNUM":3 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1033 ,"SOME_CHAR":"33 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.2035595692 ,"SOME_DATE":"1960-09-07" ,"SOME_DATETIME":"1960-01-01 11:52:19" ,"SOME_TIME":"00:00:42" ,"SOME_SHORTNUM":29 ,"SOME_BESTNUM":56 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1034 ,"SOME_CHAR":"34 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6792435556 ,"SOME_DATE":"1960-04-21" ,"SOME_DATETIME":"1960-01-01 07:17:04" ,"SOME_TIME":"00:01:14" ,"SOME_SHORTNUM":68 ,"SOME_BESTNUM":9 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1035 ,"SOME_CHAR":"35 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9494116972 ,"SOME_DATE":"1960-01-19" ,"SOME_DATETIME":"1960-01-01 10:15:38" ,"SOME_TIME":"00:01:16" ,"SOME_SHORTNUM":91 ,"SOME_BESTNUM":10 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1036 ,"SOME_CHAR":"36 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5446134911 ,"SOME_DATE":"1960-10-26" ,"SOME_DATETIME":"1960-01-01 03:55:27" ,"SOME_TIME":"00:01:24" ,"SOME_SHORTNUM":72 ,"SOME_BESTNUM":36 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1037 ,"SOME_CHAR":"37 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.458775894 ,"SOME_DATE":"1960-11-21" ,"SOME_DATETIME":"1960-01-01 13:34:37" ,"SOME_TIME":"00:01:35" ,"SOME_SHORTNUM":97 ,"SOME_BESTNUM":32 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1038 ,"SOME_CHAR":"38 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1537194239 ,"SOME_DATE":"1961-05-06" ,"SOME_DATETIME":"1960-01-01 06:14:13" ,"SOME_TIME":"00:00:29" ,"SOME_SHORTNUM":60 ,"SOME_BESTNUM":98 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1039 ,"SOME_CHAR":"39 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4935002562 ,"SOME_DATE":"1960-06-05" ,"SOME_DATETIME":"1960-01-01 06:59:42" ,"SOME_TIME":"00:00:45" ,"SOME_SHORTNUM":95 ,"SOME_BESTNUM":55 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1040 ,"SOME_CHAR":"40 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.124728859 ,"SOME_DATE":"1961-03-09" ,"SOME_DATETIME":"1960-01-01 03:03:06" ,"SOME_TIME":"00:01:23" ,"SOME_SHORTNUM":35 ,"SOME_BESTNUM":79 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1041 ,"SOME_CHAR":"41 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.2794422001 ,"SOME_DATE":"1962-07-06" ,"SOME_DATETIME":"1960-01-01 05:29:26" ,"SOME_TIME":"00:00:51" ,"SOME_SHORTNUM":86 ,"SOME_BESTNUM":66 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1042 ,"SOME_CHAR":"42 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7030775499 ,"SOME_DATE":"1960-08-11" ,"SOME_DATETIME":"1960-01-01 12:11:24" ,"SOME_TIME":"00:00:38" ,"SOME_SHORTNUM":86 ,"SOME_BESTNUM":97 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1043 ,"SOME_CHAR":"43 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0701107537 ,"SOME_DATE":"1961-01-29" ,"SOME_DATETIME":"1960-01-01 03:44:09" ,"SOME_TIME":"00:00:03" ,"SOME_SHORTNUM":25 ,"SOME_BESTNUM":8 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1044 ,"SOME_CHAR":"44 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6423292927 ,"SOME_DATE":"1962-01-15" ,"SOME_DATETIME":"1960-01-01 00:57:07" ,"SOME_TIME":"00:00:09" ,"SOME_SHORTNUM":97 ,"SOME_BESTNUM":37 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1045 ,"SOME_CHAR":"45 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7206447743 ,"SOME_DATE":"1961-10-14" ,"SOME_DATETIME":"1960-01-01 12:25:32" ,"SOME_TIME":"00:00:07" ,"SOME_SHORTNUM":58 ,"SOME_BESTNUM":58 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1046 ,"SOME_CHAR":"46 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0431997366 ,"SOME_DATE":"1960-09-12" ,"SOME_DATETIME":"1960-01-01 05:12:57" ,"SOME_TIME":"00:01:35" ,"SOME_SHORTNUM":17 ,"SOME_BESTNUM":8 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1047 ,"SOME_CHAR":"47 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.3704071368 ,"SOME_DATE":"1960-07-01" ,"SOME_DATETIME":"1960-01-01 02:44:37" ,"SOME_TIME":"00:00:06" ,"SOME_SHORTNUM":45 ,"SOME_BESTNUM":26 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1048 ,"SOME_CHAR":"48 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.654417035 ,"SOME_DATE":"1961-05-04" ,"SOME_DATETIME":"1960-01-01 01:23:07" ,"SOME_TIME":"00:01:38" ,"SOME_SHORTNUM":41 ,"SOME_BESTNUM":13 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1049 ,"SOME_CHAR":"49 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1300212565 ,"SOME_DATE":"1961-01-06" ,"SOME_DATETIME":"1960-01-01 05:27:29" ,"SOME_TIME":"00:01:21" ,"SOME_SHORTNUM":37 ,"SOME_BESTNUM":66 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1050 ,"SOME_CHAR":"50 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0058409725 ,"SOME_DATE":"1960-07-23" ,"SOME_DATETIME":"1960-01-01 00:04:24" ,"SOME_TIME":"00:00:40" ,"SOME_SHORTNUM":15 ,"SOME_BESTNUM":32 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1051 ,"SOME_CHAR":"51 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7239382587 ,"SOME_DATE":"1960-06-09" ,"SOME_DATETIME":"1960-01-01 03:15:09" ,"SOME_TIME":"00:00:04" ,"SOME_SHORTNUM":42 ,"SOME_BESTNUM":82 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1052 ,"SOME_CHAR":"52 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8319003712 ,"SOME_DATE":"1960-08-13" ,"SOME_DATETIME":"1960-01-01 07:38:35" ,"SOME_TIME":"00:00:36" ,"SOME_SHORTNUM":69 ,"SOME_BESTNUM":81 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1053 ,"SOME_CHAR":"53 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5030828875 ,"SOME_DATE":"1961-06-22" ,"SOME_DATETIME":"1960-01-01 11:25:29" ,"SOME_TIME":"00:00:53" ,"SOME_SHORTNUM":39 ,"SOME_BESTNUM":75 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1054 ,"SOME_CHAR":"54 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7148045514 ,"SOME_DATE":"1960-08-26" ,"SOME_DATETIME":"1960-01-01 10:10:09" ,"SOME_TIME":"00:00:39" ,"SOME_SHORTNUM":6 ,"SOME_BESTNUM":4 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1055 ,"SOME_CHAR":"55 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8557945787 ,"SOME_DATE":"1960-10-19" ,"SOME_DATETIME":"1960-01-01 02:17:32" ,"SOME_TIME":"00:00:08" ,"SOME_SHORTNUM":93 ,"SOME_BESTNUM":36 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1056 ,"SOME_CHAR":"56 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9700463307 ,"SOME_DATE":"1962-07-11" ,"SOME_DATETIME":"1960-01-01 11:18:41" ,"SOME_TIME":"00:00:51" ,"SOME_SHORTNUM":25 ,"SOME_BESTNUM":35 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1057 ,"SOME_CHAR":"57 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9380399426 ,"SOME_DATE":"1961-06-26" ,"SOME_DATETIME":"1960-01-01 13:15:13" ,"SOME_TIME":"00:00:52" ,"SOME_SHORTNUM":57 ,"SOME_BESTNUM":66 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1058 ,"SOME_CHAR":"58 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8484499486 ,"SOME_DATE":"1960-06-02" ,"SOME_DATETIME":"1960-01-01 01:14:51" ,"SOME_TIME":"00:00:00" ,"SOME_SHORTNUM":80 ,"SOME_BESTNUM":58 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1059 ,"SOME_CHAR":"59 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1415707628 ,"SOME_DATE":"1961-07-28" ,"SOME_DATETIME":"1960-01-01 06:33:16" ,"SOME_TIME":"00:00:58" ,"SOME_SHORTNUM":11 ,"SOME_BESTNUM":32 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1060 ,"SOME_CHAR":"60 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.282674513 ,"SOME_DATE":"1962-03-27" ,"SOME_DATETIME":"1960-01-01 00:25:37" ,"SOME_TIME":"00:00:56" ,"SOME_SHORTNUM":79 ,"SOME_BESTNUM":58 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1061 ,"SOME_CHAR":"61 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.372728008 ,"SOME_DATE":"1962-01-04" ,"SOME_DATETIME":"1960-01-01 05:07:43" ,"SOME_TIME":"00:01:00" ,"SOME_SHORTNUM":86 ,"SOME_BESTNUM":92 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1062 ,"SOME_CHAR":"62 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9517337316 ,"SOME_DATE":"1961-08-29" ,"SOME_DATETIME":"1960-01-01 02:40:05" ,"SOME_TIME":"00:00:05" ,"SOME_SHORTNUM":30 ,"SOME_BESTNUM":93 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1063 ,"SOME_CHAR":"63 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0967498683 ,"SOME_DATE":"1962-02-17" ,"SOME_DATETIME":"1960-01-01 07:30:41" ,"SOME_TIME":"00:00:29" ,"SOME_SHORTNUM":90 ,"SOME_BESTNUM":82 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1064 ,"SOME_CHAR":"64 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0540671353 ,"SOME_DATE":"1961-05-26" ,"SOME_DATETIME":"1960-01-01 13:13:43" ,"SOME_TIME":"00:00:08" ,"SOME_SHORTNUM":88 ,"SOME_BESTNUM":45 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1065 ,"SOME_CHAR":"65 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6461636464 ,"SOME_DATE":"1962-01-27" ,"SOME_DATETIME":"1960-01-01 02:56:41" ,"SOME_TIME":"00:00:19" ,"SOME_SHORTNUM":41 ,"SOME_BESTNUM":38 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1066 ,"SOME_CHAR":"66 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9053011983 ,"SOME_DATE":"1960-10-02" ,"SOME_DATETIME":"1960-01-01 03:35:49" ,"SOME_TIME":"00:01:04" ,"SOME_SHORTNUM":68 ,"SOME_BESTNUM":39 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1067 ,"SOME_CHAR":"67 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.977525881 ,"SOME_DATE":"1962-07-19" ,"SOME_DATETIME":"1960-01-01 05:53:20" ,"SOME_TIME":"00:00:28" ,"SOME_SHORTNUM":28 ,"SOME_BESTNUM":34 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1068 ,"SOME_CHAR":"68 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.2165553161 ,"SOME_DATE":"1960-05-13" ,"SOME_DATETIME":"1960-01-01 01:44:02" ,"SOME_TIME":"00:01:12" ,"SOME_SHORTNUM":63 ,"SOME_BESTNUM":23 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1069 ,"SOME_CHAR":"69 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.2248352795 ,"SOME_DATE":"1961-05-09" ,"SOME_DATETIME":"1960-01-01 00:04:33" ,"SOME_TIME":"00:00:09" ,"SOME_SHORTNUM":26 ,"SOME_BESTNUM":93 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1070 ,"SOME_CHAR":"70 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1386283367 ,"SOME_DATE":"1962-05-18" ,"SOME_DATETIME":"1960-01-01 03:32:00" ,"SOME_TIME":"00:01:36" ,"SOME_SHORTNUM":83 ,"SOME_BESTNUM":89 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1071 ,"SOME_CHAR":"71 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9337331415 ,"SOME_DATE":"1961-05-16" ,"SOME_DATETIME":"1960-01-01 13:46:54" ,"SOME_TIME":"00:00:47" ,"SOME_SHORTNUM":27 ,"SOME_BESTNUM":56 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1072 ,"SOME_CHAR":"72 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0352235506 ,"SOME_DATE":"1961-06-06" ,"SOME_DATETIME":"1960-01-01 09:09:20" ,"SOME_TIME":"00:01:16" ,"SOME_SHORTNUM":7 ,"SOME_BESTNUM":27 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1073 ,"SOME_CHAR":"73 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.3206662695 ,"SOME_DATE":"1960-03-13" ,"SOME_DATETIME":"1960-01-01 10:38:11" ,"SOME_TIME":"00:01:08" ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":50 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1074 ,"SOME_CHAR":"74 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4610861705 ,"SOME_DATE":"1961-08-31" ,"SOME_DATETIME":"1960-01-01 09:35:41" ,"SOME_TIME":"00:01:08" ,"SOME_SHORTNUM":54 ,"SOME_BESTNUM":68 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1075 ,"SOME_CHAR":"75 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4527745622 ,"SOME_DATE":"1962-01-16" ,"SOME_DATETIME":"1960-01-01 06:49:27" ,"SOME_TIME":"00:00:45" ,"SOME_SHORTNUM":96 ,"SOME_BESTNUM":63 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1076 ,"SOME_CHAR":"76 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.3581244058 ,"SOME_DATE":"1960-05-16" ,"SOME_DATETIME":"1960-01-01 00:56:40" ,"SOME_TIME":"00:01:13" ,"SOME_SHORTNUM":72 ,"SOME_BESTNUM":24 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1077 ,"SOME_CHAR":"77 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8939921334 ,"SOME_DATE":"1961-01-21" ,"SOME_DATETIME":"1960-01-01 09:16:31" ,"SOME_TIME":"00:01:15" ,"SOME_SHORTNUM":88 ,"SOME_BESTNUM":69 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1078 ,"SOME_CHAR":"78 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.2445727066 ,"SOME_DATE":"1960-12-22" ,"SOME_DATETIME":"1960-01-01 03:11:14" ,"SOME_TIME":"00:01:37" ,"SOME_SHORTNUM":88 ,"SOME_BESTNUM":32 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1079 ,"SOME_CHAR":"79 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9683029465 ,"SOME_DATE":"1961-08-14" ,"SOME_DATETIME":"1960-01-01 04:45:43" ,"SOME_TIME":"00:01:09" ,"SOME_SHORTNUM":51 ,"SOME_BESTNUM":60 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1080 ,"SOME_CHAR":"80 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1303541368 ,"SOME_DATE":"1962-02-28" ,"SOME_DATETIME":"1960-01-01 02:14:50" ,"SOME_TIME":"00:00:21" ,"SOME_SHORTNUM":79 ,"SOME_BESTNUM":87 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1081 ,"SOME_CHAR":"81 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7656979653 ,"SOME_DATE":"1961-08-03" ,"SOME_DATETIME":"1960-01-01 06:49:50" ,"SOME_TIME":"00:01:31" ,"SOME_SHORTNUM":58 ,"SOME_BESTNUM":30 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1082 ,"SOME_CHAR":"82 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1855629674 ,"SOME_DATE":"1960-12-16" ,"SOME_DATETIME":"1960-01-01 06:27:21" ,"SOME_TIME":"00:00:33" ,"SOME_SHORTNUM":1 ,"SOME_BESTNUM":72 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1083 ,"SOME_CHAR":"83 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4782178642 ,"SOME_DATE":"1961-04-16" ,"SOME_DATETIME":"1960-01-01 08:05:23" ,"SOME_TIME":"00:01:10" ,"SOME_SHORTNUM":0 ,"SOME_BESTNUM":1 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1084 ,"SOME_CHAR":"84 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1670272132 ,"SOME_DATE":"1962-06-21" ,"SOME_DATETIME":"1960-01-01 13:43:20" ,"SOME_TIME":"00:00:27" ,"SOME_SHORTNUM":53 ,"SOME_BESTNUM":6 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1085 ,"SOME_CHAR":"85 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6068249189 ,"SOME_DATE":"1960-05-21" ,"SOME_DATETIME":"1960-01-01 11:05:11" ,"SOME_TIME":"00:00:08" ,"SOME_SHORTNUM":17 ,"SOME_BESTNUM":68 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1086 ,"SOME_CHAR":"86 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0936049917 ,"SOME_DATE":"1962-07-20" ,"SOME_DATETIME":"1960-01-01 07:16:09" ,"SOME_TIME":"00:00:46" ,"SOME_SHORTNUM":73 ,"SOME_BESTNUM":37 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1087 ,"SOME_CHAR":"87 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6538249178 ,"SOME_DATE":"1960-04-24" ,"SOME_DATETIME":"1960-01-01 02:06:54" ,"SOME_TIME":"00:00:59" ,"SOME_SHORTNUM":95 ,"SOME_BESTNUM":32 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1088 ,"SOME_CHAR":"88 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8846158562 ,"SOME_DATE":"1961-11-19" ,"SOME_DATETIME":"1960-01-01 05:35:27" ,"SOME_TIME":"00:01:01" ,"SOME_SHORTNUM":87 ,"SOME_BESTNUM":30 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1089 ,"SOME_CHAR":"89 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1578208316 ,"SOME_DATE":"1961-03-03" ,"SOME_DATETIME":"1960-01-01 09:02:02" ,"SOME_TIME":"00:00:23" ,"SOME_SHORTNUM":60 ,"SOME_BESTNUM":53 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1090 ,"SOME_CHAR":"90 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4225753753 ,"SOME_DATE":"1960-03-19" ,"SOME_DATETIME":"1960-01-01 12:14:04" ,"SOME_TIME":"00:01:00" ,"SOME_SHORTNUM":57 ,"SOME_BESTNUM":64 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1091 ,"SOME_CHAR":"91 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6598943354 ,"SOME_DATE":"1961-09-17" ,"SOME_DATETIME":"1960-01-01 03:03:13" ,"SOME_TIME":"00:01:00" ,"SOME_SHORTNUM":41 ,"SOME_BESTNUM":28 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1092 ,"SOME_CHAR":"92 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6293501689 ,"SOME_DATE":"1961-10-18" ,"SOME_DATETIME":"1960-01-01 00:21:13" ,"SOME_TIME":"00:01:11" ,"SOME_SHORTNUM":64 ,"SOME_BESTNUM":7 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1093 ,"SOME_CHAR":"93 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4378844986 ,"SOME_DATE":"1961-06-24" ,"SOME_DATETIME":"1960-01-01 10:20:39" ,"SOME_TIME":"00:00:27" ,"SOME_SHORTNUM":30 ,"SOME_BESTNUM":78 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1094 ,"SOME_CHAR":"94 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9838584969 ,"SOME_DATE":"1962-05-25" ,"SOME_DATETIME":"1960-01-01 02:59:06" ,"SOME_TIME":"00:00:59" ,"SOME_SHORTNUM":48 ,"SOME_BESTNUM":98 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1095 ,"SOME_CHAR":"95 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.089252377 ,"SOME_DATE":"1961-06-16" ,"SOME_DATETIME":"1960-01-01 04:54:20" ,"SOME_TIME":"00:00:10" ,"SOME_SHORTNUM":75 ,"SOME_BESTNUM":33 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1096 ,"SOME_CHAR":"96 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4578205154 ,"SOME_DATE":"1960-01-20" ,"SOME_DATETIME":"1960-01-01 10:36:00" ,"SOME_TIME":"00:00:41" ,"SOME_SHORTNUM":14 ,"SOME_BESTNUM":17 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1097 ,"SOME_CHAR":"97 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5863271587 ,"SOME_DATE":"1962-04-20" ,"SOME_DATETIME":"1960-01-01 11:14:11" ,"SOME_TIME":"00:01:28" ,"SOME_SHORTNUM":66 ,"SOME_BESTNUM":84 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1098 ,"SOME_CHAR":"98 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.2994232058 ,"SOME_DATE":"1960-07-04" ,"SOME_DATETIME":"1960-01-01 08:15:41" ,"SOME_TIME":"00:01:28" ,"SOME_SHORTNUM":99 ,"SOME_BESTNUM":85 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1099 ,"SOME_CHAR":"99 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0981378053 ,"SOME_DATE":"1960-02-05" ,"SOME_DATETIME":"1960-01-01 11:10:11" ,"SOME_TIME":"00:00:43" ,"SOME_SHORTNUM":23 ,"SOME_BESTNUM":65 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":10100 ,"SOME_CHAR":"100 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9829722652 ,"SOME_DATE":"1960-02-01" ,"SOME_DATETIME":"1960-01-01 05:45:06" ,"SOME_TIME":"00:01:16" ,"SOME_SHORTNUM":28 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":10101 ,"SOME_CHAR":"101 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4540794913 ,"SOME_DATE":"1962-08-03" ,"SOME_DATETIME":"1960-01-01 09:27:03" ,"SOME_TIME":"00:01:10" ,"SOME_SHORTNUM":42 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":10102 ,"SOME_CHAR":"102 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8452174369 ,"SOME_DATE":"1960-10-02" ,"SOME_DATETIME":"1960-01-01 03:08:47" ,"SOME_TIME":"00:00:23" ,"SOME_SHORTNUM":10 ,"SOME_BESTNUM":14 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":10103 ,"SOME_CHAR":"103 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5904919606 ,"SOME_DATE":"1960-07-30" ,"SOME_DATETIME":"1960-01-01 13:09:58" ,"SOME_TIME":"00:00:10" ,"SOME_SHORTNUM":68 ,"SOME_BESTNUM":53 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":10104 ,"SOME_CHAR":"104 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7083388677 ,"SOME_DATE":"1960-07-24" ,"SOME_DATETIME":"1960-01-01 05:49:50" ,"SOME_TIME":"00:01:29" ,"SOME_SHORTNUM":84 ,"SOME_BESTNUM":33 }
]
, "$sasdata":{"vars":{
"_____DELETE__THIS__RECORD_____" :{"format":"$3." ,"label":"_____DELETE__THIS__RECORD_____" ,"length":"3" ,"type":"char" }
,"PRIMARY_KEY_FIELD" :{"format":"best." ,"label":"PRIMARY_KEY_FIELD" ,"length":"8" ,"type":"num" }
,"SOME_CHAR" :{"format":"$32767." ,"label":"SOME_CHAR" ,"length":"32767" ,"type":"char" }
,"SOME_DROPDOWN" :{"format":"$128." ,"label":"SOME_DROPDOWN" ,"length":"128" ,"type":"char" }
,"SOME_NUM" :{"format":"best." ,"label":"SOME_NUM" ,"length":"8" ,"type":"num" }
,"SOME_DATE" :{"format":"$200." ,"label":"SOME_DATE" ,"length":"200" ,"type":"char" }
,"SOME_DATETIME" :{"format":"$200." ,"label":"SOME_DATETIME" ,"length":"200" ,"type":"char" }
,"SOME_TIME" :{"format":"$200." ,"label":"SOME_TIME" ,"length":"200" ,"type":"char" }
,"SOME_SHORTNUM" :{"format":"best." ,"label":"SOME_SHORTNUM" ,"length":"4" ,"type":"num" }
,"SOME_BESTNUM" :{"format":"BEST." ,"label":"SOME_BESTNUM" ,"length":"8" ,"type":"num" }
}}
, "sasparams":
[
{
"COLHEADERS": "_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM",
"FILTER_TEXT": "",
"PKCNT": 1,
"PK": "PRIMARY_KEY_FIELD",
"DTVARS": " SOME_DATE",
"DTTMVARS": " SOME_DATETIME",
"TMVARS": " SOME_TIME",
"COLTYPE": "{\\"data\\":\\"_____DELETE__THIS__RECORD_____\\",\\"type\\":\\"dropdown\\",\\"source\\":[\\"No\\",\\"Yes\\"]},{\\"data\\":\\"PRIMARY_KEY_FIELD\\",\\"type\\":\\"numeric\\",\\"format\\":\\"0\\"},{\\"data\\":\\"SOME_CHAR\\"},{\\"data\\":\\"SOME_DROPDOWN\\"},{\\"data\\":\\"SOME_NUM\\",\\"type\\":\\"numeric\\",\\"format\\":\\"0\\"},{\\"data\\":\\"SOME_DATE\\",\\"type\\":\\"date\\",\\"dateFormat\\":\\"YYYY-MM-DD\\",\\"correctFormat\\":\\"true\\"},{\\"data\\":\\"SOME_DATETIME\\",\\"type\\":\\"date\\",\\"dateFormat\\":\\"YYYY-MM-DD HH:mm:ss\\",\\"correctFormat\\":\\"true\\"},{\\"data\\":\\"SOME_TIME\\",\\"type\\":\\"time\\",\\"timeFormat\\":\\"HH:mm:ss\\",\\"correctFormat\\":\\"true\\"},{\\"data\\":\\"SOME_SHORTNUM\\",\\"type\\":\\"numeric\\",\\"format\\":\\"0\\"},{\\"data\\":\\"SOME_BESTNUM\\",\\"type\\":\\"numeric\\",\\"format\\":\\"0\\"}",
"LOADTYPE": "UPDATE",
"RK_FLAG": 0,
"CLS_FLAG": 0
const data = {
SYSDATE: "26SEP22",
SYSTIME: "08:30",
approvers: [
{ PERSONNAME: "sasdemo", EMAIL: "sasdemo", USERID: "sasdemo" }
],
cols: [
{
NAME: "PRIMARY_KEY_FIELD",
VARNUM: 1,
LABEL: "PRIMARY_KEY_FIELD",
FMTNAME: "",
DDTYPE: "NUMERIC",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_BESTNUM",
VARNUM: 9,
LABEL: "SOME_BESTNUM",
FMTNAME: "BEST",
DDTYPE: "NUMERIC",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_CHAR",
VARNUM: 2,
LABEL: "SOME_CHAR",
FMTNAME: "",
DDTYPE: "CHARACTER",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_DATE",
VARNUM: 5,
LABEL: "SOME_DATE",
FMTNAME: "DATE",
DDTYPE: "DATE",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_DATETIME",
VARNUM: 6,
LABEL: "SOME_DATETIME",
FMTNAME: "DATETIME",
DDTYPE: "DATETIME",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_DROPDOWN",
VARNUM: 3,
LABEL: "SOME_DROPDOWN",
FMTNAME: "",
DDTYPE: "CHARACTER",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_HARDSELECT",
VARNUM: 10,
LABEL: "SOME_HARDSELECT",
FMTNAME: "",
DDTYPE: "CHARACTER",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_NUM",
VARNUM: 4,
LABEL: "SOME_NUM",
FMTNAME: "",
DDTYPE: "NUMERIC",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_SHORTNUM",
VARNUM: 8,
LABEL: "SOME_SHORTNUM",
FMTNAME: "",
DDTYPE: "NUMERIC",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_TIME",
VARNUM: 7,
LABEL: "SOME_TIME",
FMTNAME: "TIME",
DDTYPE: "TIME",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "READONLY_COL",
VARNUM: 11,
LABEL: "READONLY_COL",
FMTNAME: "",
DDTYPE: "CHARACTER",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "Read-only: default value inserted on add-row, not editable",
LONGDESC: ""
},
{
NAME: "HIDDEN_COL",
VARNUM: 12,
LABEL: "HIDDEN_COL",
FMTNAME: "",
DDTYPE: "CHARACTER",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "Hidden: invisible in grid but submitted; default on add-row",
LONGDESC: ""
},
{
NAME: "ROUND_COL",
VARNUM: 13,
LABEL: "ROUND_COL",
FMTNAME: "",
DDTYPE: "NUMERIC",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "Round: edited values rounded Excel-style to 2 decimals",
LONGDESC: ""
},
{
NAME: "NUMFMT_COL",
VARNUM: 14,
LABEL: "NUMFMT_COL",
FMTNAME: "",
DDTYPE: "NUMERIC",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "Number format: displayed as EUR currency (value unchanged)",
LONGDESC: ""
}
],
dqdata: [
{ BASE_COL: "SOME_DROPDOWN", RULE_VALUE: "SOME_DROPDOWN", RULE_DATA: "Option 1", SELECTBOX_ORDER: 1 },
{ BASE_COL: "SOME_DROPDOWN", RULE_VALUE: "SOME_DROPDOWN", RULE_DATA: "Option 2", SELECTBOX_ORDER: 2 },
{ BASE_COL: "SOME_DROPDOWN", RULE_VALUE: "SOME_DROPDOWN", RULE_DATA: "Option 3", SELECTBOX_ORDER: 2 },
{
BASE_COL: "SOME_DROPDOWN",
RULE_VALUE: "SOME_DROPDOWN",
RULE_DATA: "This is a long option. This option is very long. It is optional, though.",
SELECTBOX_ORDER: 3
},
{ BASE_COL: "SOME_HARDSELECT", RULE_VALUE: "SOME_HARDSELECT", RULE_DATA: "Alpha", SELECTBOX_ORDER: 1 },
{ BASE_COL: "SOME_HARDSELECT", RULE_VALUE: "SOME_HARDSELECT", RULE_DATA: "Bravo", SELECTBOX_ORDER: 2 },
{ BASE_COL: "SOME_HARDSELECT", RULE_VALUE: "SOME_HARDSELECT", RULE_DATA: "Charlie", SELECTBOX_ORDER: 3 }
],
dqrules: [
{ BASE_COL: "PRIMARY_KEY_FIELD", RULE_TYPE: "NOTNULL", RULE_VALUE: "", X: 0 },
{ BASE_COL: "SOME_NUM", RULE_TYPE: "HARDSELECT_HOOK", RULE_VALUE: "services/validations/mpe_x_test.some_num", X: 0 },
{ BASE_COL: "SOME_HARDSELECT", RULE_TYPE: "HARDSELECT", RULE_VALUE: "", X: 0 },
{ BASE_COL: "READONLY_COL", RULE_TYPE: "READONLY", RULE_VALUE: "Readonly default", X: 0 },
{ BASE_COL: "HIDDEN_COL", RULE_TYPE: "HIDDEN", RULE_VALUE: "Hidden default", X: 0 },
{ BASE_COL: "ROUND_COL", RULE_TYPE: "ROUND", RULE_VALUE: "2", X: 0 },
{ BASE_COL: "NUMFMT_COL", RULE_TYPE: "NUMBER_FORMAT", RULE_VALUE: '{"style":"currency","currency":"EUR"}', X: 0 }
],
dsmeta: [
{ ODS_TABLE: "ATTRIBUTES", NAME: "Data Set Name", VALUE: "DC996664.MPE_X_TEST" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Observations", VALUE: "496" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Member Type", VALUE: "DATA" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Variables", VALUE: "9" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Engine", VALUE: "V9" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Indexes", VALUE: "1" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Created", VALUE: "09/26/2022 08:24:39" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Integrity Constraints", VALUE: "1" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Last Modified", VALUE: "09/26/2022 08:24:45" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Observation Length", VALUE: "32947" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Protection", VALUE: " ." },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Deleted Observations", VALUE: "0" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Data Set Type", VALUE: " ." },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Compressed", VALUE: "CHAR" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Label", VALUE: " ." },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Reuse Space", VALUE: "NO" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Data Representation", VALUE: "WINDOWS_64" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Point to Observations", VALUE: "YES" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Encoding", VALUE: "wlatin1 Western (Windows)" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Sorted", VALUE: "NO" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Data Set Page Size", VALUE: "262144" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Number of Data Set Pages", VALUE: "3" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Index File Page Size", VALUE: "4096" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Number of Index File Pages", VALUE: "4" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Number of Data Set Repairs", VALUE: "0" },
{ ODS_TABLE: "ENGINEHOST", NAME: "ExtendObsCounter", VALUE: "YES" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Filename", VALUE: "C:DataControllerDC996664mpe_x_test.sas7bdat" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Release Created", VALUE: "9.0401M7" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Host Created", VALUE: "X64_DSRV16" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Owner Name", VALUE: "BUILTINAdministrators" },
{ ODS_TABLE: "ENGINEHOST", NAME: "File Size", VALUE: " 1MB" },
{ ODS_TABLE: "ENGINEHOST", NAME: "File Size (bytes)", VALUE: "1048576" }
],
maxvarlengths: [
{ NAME: "_____DELETE__THIS__RECORD_____", MAXLEN: 3 },
{ NAME: "PRIMARY_KEY_FIELD", MAXLEN: 4 },
{ NAME: "some_char", MAXLEN: 591 },
{ NAME: "some_dropdown", MAXLEN: 8 },
{ NAME: "some_hardselect", MAXLEN: 7 },
{ NAME: "some_num", MAXLEN: 8 },
{ NAME: "some_date", MAXLEN: 10 },
{ NAME: "some_datetime", MAXLEN: 19 },
{ NAME: "some_time", MAXLEN: 8 },
{ NAME: "some_shortnum", MAXLEN: 3 },
{ NAME: "some_bestnum", MAXLEN: 3 },
{ NAME: "readonly_col", MAXLEN: 16 },
{ NAME: "hidden_col", MAXLEN: 14 },
{ NAME: "round_col", MAXLEN: 8 },
{ NAME: "numfmt_col", MAXLEN: 8 }
],
query: [],
sasdata: makeRows(100),
$sasdata: {
vars: {
_____DELETE__THIS__RECORD_____: { format: "$3.", label: "_____DELETE__THIS__RECORD_____", length: "3", type: "char" },
PRIMARY_KEY_FIELD: { format: "best.", label: "PRIMARY_KEY_FIELD", length: "8", type: "num" },
SOME_CHAR: { format: "$32767.", label: "SOME_CHAR", length: "32767", type: "char" },
SOME_DROPDOWN: { format: "$128.", label: "SOME_DROPDOWN", length: "128", type: "char" },
SOME_HARDSELECT: { format: "$128.", label: "SOME_HARDSELECT", length: "128", type: "char" },
SOME_NUM: { format: "best.", label: "SOME_NUM", length: "8", type: "num" },
SOME_DATE: { format: "$200.", label: "SOME_DATE", length: "200", type: "char" },
SOME_DATETIME: { format: "$200.", label: "SOME_DATETIME", length: "200", type: "char" },
SOME_TIME: { format: "$200.", label: "SOME_TIME", length: "200", type: "char" },
SOME_SHORTNUM: { format: "best.", label: "SOME_SHORTNUM", length: "4", type: "num" },
SOME_BESTNUM: { format: "BEST.", label: "SOME_BESTNUM", length: "8", type: "num" },
READONLY_COL: { format: "$200.", label: "READONLY_COL", length: "200", type: "char" },
HIDDEN_COL: { format: "$200.", label: "HIDDEN_COL", length: "200", type: "char" },
ROUND_COL: { format: "best.", label: "ROUND_COL", length: "8", type: "num" },
NUMFMT_COL: { format: "best.", label: "NUMFMT_COL", length: "8", type: "num" }
}
},
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",
FILTER_TEXT: "",
PKCNT: 1,
PK: "PRIMARY_KEY_FIELD",
DTVARS: " SOME_DATE",
DTTMVARS: " SOME_DATETIME",
TMVARS: " SOME_TIME",
COLTYPE: "{\"data\":\"_____DELETE__THIS__RECORD_____\",\"type\":\"dropdown\",\"source\":[\"No\",\"Yes\"]},{\"data\":\"PRIMARY_KEY_FIELD\",\"type\":\"numeric\",\"format\":\"0\"},{\"data\":\"SOME_CHAR\"},{\"data\":\"SOME_DROPDOWN\"},{\"data\":\"SOME_HARDSELECT\"},{\"data\":\"SOME_NUM\",\"type\":\"numeric\",\"format\":\"0\"},{\"data\":\"SOME_DATE\",\"type\":\"date\",\"dateFormat\":\"YYYY-MM-DD\",\"correctFormat\":\"true\"},{\"data\":\"SOME_DATETIME\",\"type\":\"date\",\"dateFormat\":\"YYYY-MM-DD HH:mm:ss\",\"correctFormat\":\"true\"},{\"data\":\"SOME_TIME\",\"type\":\"time\",\"timeFormat\":\"HH:mm:ss\",\"correctFormat\":\"true\"},{\"data\":\"SOME_SHORTNUM\",\"type\":\"numeric\",\"format\":\"0\"},{\"data\":\"SOME_BESTNUM\",\"type\":\"numeric\",\"format\":\"0\"},{\"data\":\"READONLY_COL\"},{\"data\":\"HIDDEN_COL\"},{\"data\":\"ROUND_COL\",\"type\":\"numeric\",\"format\":\"0\"},{\"data\":\"NUMFMT_COL\",\"type\":\"numeric\",\"format\":\"0\"}",
LOADTYPE: "UPDATE",
RK_FLAG: 0,
CLS_FLAG: 0
}
],
xl_rules: [],
_DEBUG: "",
_METAUSER: "sasdemo@SAS",
_METAPERSON: "sasdemo",
_PROGRAM: "/Projects/app/dc/services/editors/getdata",
AUTOEXEC: "D%3A%5Copt%5Csasinside%5CConfig%5CLev1%5CSASApp%5CStoredProcessServer%5Cautoexec.sas",
MF_GETUSER: "sasdemo",
SYSCC: "0",
SYSENCODING: "wlatin1",
SYSERRORTEXT: "",
SYSHOSTNAME: "SAS",
SYSPROCESSID: "41DD8056A491DB23409E940000000000",
SYSPROCESSMODE: "SAS Stored Process Server",
SYSPROCESSNAME: "",
SYSJOBID: "27448",
SYSSCPL: "X64_DSRV16",
SYSSITE: "123",
SYSUSERID: "sassrv",
SYSVLONG: "9.04.01M7P080520",
SYSWARNINGTEXT: "ENCODING option ignored for files opened with RECFM=N.",
END_DTTM: "2022-09-26T08:30:13.853000",
MEMSIZE: "46GB"
}
]
, "xl_rules":
[
]
,"_DEBUG" : ""
,"_METAUSER": "sasdemo@SAS"
,"_METAPERSON": "sasdemo"
,"_PROGRAM" : "/Projects/app/dc/services/editors/getdata"
,"AUTOEXEC" : "D%3A%5Copt%5Csasinside%5CConfig%5CLev1%5CSASApp%5CStoredProcessServer%5Cautoexec.sas"
,"MF_GETUSER" : "sasdemo"
,"SYSCC" : "0"
,"SYSENCODING" : "wlatin1"
,"SYSERRORTEXT" : ""
,"SYSHOSTNAME" : "SAS"
,"SYSPROCESSID" : "41DD8056A491DB23409E940000000000"
,"SYSPROCESSMODE" : "SAS Stored Process Server"
,"SYSPROCESSNAME" : ""
,"SYSJOBID" : "27448"
,"SYSSCPL" : "X64_DSRV16"
,"SYSSITE" : "123"
,"SYSUSERID" : "sassrv"
,"SYSVLONG" : "9.04.01M7P080520"
,"SYSWARNINGTEXT" : "ENCODING option ignored for files opened with RECFM=N."
,"END_DTTM" : "2022-09-26T08:30:13.853000"
,"MEMSIZE" : "46GB"
}`
_webout = JSON.stringify(data)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -5381,6 +5381,44 @@ let webouts = {
}`
}
/**
* Mirror the editor's demo columns (READONLY/HIDDEN/ROUND/NUMBER_FORMAT) into
* the view payload so the View and Edit screens show the same columns.
*
* The viewer is read-only: it ignores READONLY/ROUND (editor-only), but it DOES
* honour NUMBER_FORMAT (display) and HIDDEN (visibility) via `dqrules` below, so
* the currency column renders and the hidden column is hidden, same as the editor.
* The MPE_X_TEST entry is a JSON string, so we parse, augment
* `cols`/`viewdata`/`dqrules`, then re-stringify.
*/
;(() => {
const v = JSON.parse(webouts.MPE_X_TEST)
v.cols.push(
{ NAME: "READONLY_COL", LENGTH: 200, VARNUM: 11, LABEL: "READONLY_COL", FMTNAME: "", FORMAT: "$200.", TYPE: "C", DDTYPE: "CHARACTER" },
{ NAME: "HIDDEN_COL", LENGTH: 200, VARNUM: 12, LABEL: "HIDDEN_COL", FMTNAME: "", FORMAT: "$200.", TYPE: "C", DDTYPE: "CHARACTER" },
{ NAME: "ROUND_COL", LENGTH: 8, VARNUM: 13, LABEL: "ROUND_COL", FMTNAME: "", FORMAT: "BEST.", TYPE: "N", DDTYPE: "NUMERIC" },
{ NAME: "NUMFMT_COL", LENGTH: 8, VARNUM: 14, LABEL: "NUMFMT_COL", FMTNAME: "", FORMAT: "BEST.", TYPE: "N", DDTYPE: "NUMERIC" }
)
v.viewdata = v.viewdata.map((row, i) => ({
...row,
READONLY_COL: "Readonly default",
HIDDEN_COL: "Hidden default",
ROUND_COL: Number((i + 1 + i / 7).toFixed(5)),
NUMFMT_COL: 1000 + i * 12.5
}))
// NUMBER_FORMAT (display) and HIDDEN (visibility) are honoured by the viewer
// (see viewer.component.ts); READONLY/ROUND are editor-only and omitted here.
v.dqrules = (v.dqrules || []).concat([
{ BASE_COL: "NUMFMT_COL", RULE_TYPE: "NUMBER_FORMAT", RULE_VALUE: '{"style":"currency","currency":"EUR"}', X: 0 },
{ BASE_COL: "HIDDEN_COL", RULE_TYPE: "HIDDEN", RULE_VALUE: "Hidden default", X: 0 }
])
webouts.MPE_X_TEST = JSON.stringify(v)
})()
let table = 'MPE_X_TEST'
if (_WEBIN_FILEREF1) {