Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ea604f9fb | |||
| 9d97bf7ea1 | |||
| eb015d712b | |||
| 1d04f4a42c | |||
| 11ee49a57a | |||
| 609731ff99 | |||
| d6cb32ed25 | |||
| 3668a7426f | |||
| cc82dcaafe | |||
| ea03bdecc5 | |||
| 51071b463b | |||
| ac0bd10212 | |||
| 1b73e355b7 | |||
| b661580c60 | |||
| dc4e07a692 | |||
| f2313b31f1 | |||
| f8810ee7e9 | |||
| 8ab4af8397 | |||
| 2382a559a5 | |||
| 5d889d824c | |||
| bed21122ce | |||
| ea8cf71101 | |||
| f1a26e132e | |||
| 1db6984de3 | |||
| 636ff237dd | |||
| 02963ab6d5 | |||
| d40f61292a | |||
| 7d94cb2ae4 | |||
| bb80476767 | |||
| 1635bc9c45 | |||
| f031b4eb89 | |||
| 93d4ab65ac | |||
| ce921a032a | |||
| 322f904b4b | |||
| 982eeac58c | |||
| 0ab9717556 | |||
| 24a85de8e1 | |||
| 65f0b979a4 | |||
| 947f34a0ad | |||
| 0f60fd7181 | |||
| 251062e42e | |||
| 05a328976e | |||
| 503cb08b2f | |||
| f71be20476 | |||
| e6397cecc1 | |||
| 80ce80ece4 | |||
| 9546fcd631 | |||
| b79aaf4327 | |||
| 76f9198f73 | |||
| d60029deae | |||
| d26f7d2511 | |||
| 33efe09b50 | |||
| e0aef9bf00 | |||
| 02d1a2e0b1 | |||
| 4e3154e929 | |||
| 32c0713256 | |||
| defe15bcec | |||
| 6f8e471f16 | |||
| dc35abfd85 | |||
| 04a8c5d52a | |||
| 2cb370053d | |||
| 1707f3802a | |||
| c87ba660ca | |||
| ef8a2dbc38 | |||
| 40d04a53c4 | |||
| d5ebb01ce3 | |||
| ec66631a33 | |||
| d66eb5dfc2 | |||
| 731b589ed8 | |||
| fe92d5fc36 | |||
| a335b400f1 | |||
| f63e507ddf | |||
| 991cc0567d | |||
| 52d58036a4 | |||
| 26bff85792 | |||
| 2ccf0d1100 | |||
| 3be33186bc | |||
| 1a7f950ae2 | |||
| 8924dc8ab1 | |||
| 0b0db1c543 | |||
| 80039f4876 | |||
| 326c26fddf |
@@ -3,7 +3,7 @@ run-name: Running Lint Check and Licence checker on Pull Request
|
||||
on: [pull_request]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '24.5.0'
|
||||
NODE_VERSION: '24.15.0'
|
||||
|
||||
jobs:
|
||||
Build-and-ng-test:
|
||||
@@ -22,30 +22,33 @@ jobs:
|
||||
apt install -y ./google-chrome*.deb
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: echo "$NPMRC" > client/.npmrc
|
||||
run: echo "$NPMRC" >> client/.npmrc
|
||||
shell: bash
|
||||
env:
|
||||
NPMRC: ${{ secrets.NPMRC}}
|
||||
|
||||
- name: Check audit
|
||||
# Audit should fail and stop the CI if critical vulnerability found
|
||||
run: |
|
||||
npm audit --omit=dev
|
||||
cd ./sas
|
||||
npm audit --omit=dev
|
||||
cd ../client
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
|
||||
- name: Lint check
|
||||
run: npm run lint:check
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd client
|
||||
# Decrypt and Install sheet
|
||||
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
||||
echo "${{ secrets.SHEET_PWD }}" | \
|
||||
gpg --batch --yes --passphrase-fd 0 \
|
||||
--output ./libraries/sheet-crypto.tgz \
|
||||
--decrypt ./libraries/sheet-crypto.tgz.gpg
|
||||
npm ci
|
||||
|
||||
- name: Check audit
|
||||
# Audit should fail and stop the CI if critical vulnerability found
|
||||
run: |
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
cd ./sas
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
cd ../client
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
|
||||
- name: Lint check
|
||||
run: npm run lint:check
|
||||
|
||||
- name: Licence checker
|
||||
run: |
|
||||
cd client
|
||||
@@ -139,6 +142,7 @@ jobs:
|
||||
- name: Zip Cypress videos
|
||||
if: always()
|
||||
run: |
|
||||
mkdir -p ./client/cypress/videos
|
||||
zip -r cypress-videos ./client/cypress/videos
|
||||
|
||||
- name: Add cypress videos artifacts
|
||||
|
||||
@@ -3,7 +3,7 @@ run-name: Running Lighthouse Performance and Accessibility Checks on Pull Reques
|
||||
on: [pull_request]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '24.5.0'
|
||||
NODE_VERSION: '24.15.0'
|
||||
|
||||
jobs:
|
||||
lighthouse:
|
||||
@@ -48,7 +48,10 @@ jobs:
|
||||
run: |
|
||||
cd client
|
||||
# Decrypt and Install sheet
|
||||
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
||||
echo "${{ secrets.SHEET_PWD }}" | \
|
||||
gpg --batch --yes --passphrase-fd 0 \
|
||||
--output ./libraries/sheet-crypto.tgz \
|
||||
--decrypt ./libraries/sheet-crypto.tgz.gpg
|
||||
npm ci
|
||||
npm install -g replace-in-files-cli
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ jobs:
|
||||
- name: Zip Cypress videos
|
||||
if: always()
|
||||
run: |
|
||||
mkdir -p ./client/cypress/videos
|
||||
zip -r cypress-videos ./client/cypress/videos
|
||||
|
||||
- name: Add cypress videos artifacts
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
legacy-peer-deps=true
|
||||
ignore-scripts=true
|
||||
save-exact=true
|
||||
fund=false
|
||||
@@ -1,3 +1,108 @@
|
||||
## [7.8.2](https://git.datacontroller.io/dc/dc/compare/v7.8.1...v7.8.2) (2026-05-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bumping ws package ([2382a55](https://git.datacontroller.io/dc/dc/commit/2382a559a5ac32b0f815776a90207650d5809ba6))
|
||||
* enabling version restore for non admin users ([5d889d8](https://git.datacontroller.io/dc/dc/commit/5d889d824cc2f8e4ea089cbb578453125dc4ba6c))
|
||||
|
||||
## [7.8.1](https://git.datacontroller.io/dc/dc/compare/v7.8.0...v7.8.1) (2026-05-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **sasjs:** enable runAsTask ([f1a26e1](https://git.datacontroller.io/dc/dc/commit/f1a26e132eba7fa2ac64754940b52ea46c6619b3))
|
||||
|
||||
# [7.8.0](https://git.datacontroller.io/dc/dc/compare/v7.7.3...v7.8.0) (2026-05-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enabling DSN=*ALL* in MPE_SECURITY ([7d94cb2](https://git.datacontroller.io/dc/dc/commit/7d94cb2ae4a3f6c1fa1011ae0fced7083a2f2793))
|
||||
* providing default values for RULE_ACTIVE on MPE_VALIDATIONS ([f031b4e](https://git.datacontroller.io/dc/dc/commit/f031b4eb8925397e60dcc739a721cfbbb6da8dff))
|
||||
* switch away from api usage for CASLIB metadata ([ce921a0](https://git.datacontroller.io/dc/dc/commit/ce921a032a8970b8078a463a41da884e1fa71bc3))
|
||||
* use correct debug param for runAsTask ([bb80476](https://git.datacontroller.io/dc/dc/commit/bb8047676749814d3b86eea666726dbe4bf5f270))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add runAsTask config attribute parser ([1635bc9](https://git.datacontroller.io/dc/dc/commit/1635bc9c451bc221f386241007f594096f114b4f))
|
||||
* enabling *ALL* option by default in MPE_SECURITY (DSN col) ([93d4ab6](https://git.datacontroller.io/dc/dc/commit/93d4ab65acce7b5b35e448146f9893964ad2cca3))
|
||||
|
||||
## [7.7.3](https://git.datacontroller.io/dc/dc/compare/v7.7.2...v7.7.3) (2026-05-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* move cas session assign to settings.sas and abort when lib is unassigned ([65f0b97](https://git.datacontroller.io/dc/dc/commit/65f0b979a401277b3e070d409659ae3fae2ff8c0))
|
||||
|
||||
## [7.7.2](https://git.datacontroller.io/dc/dc/compare/v7.7.1...v7.7.2) (2026-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client:** bundle Metropolis font locally to satisfy CSP ([9546fcd](https://git.datacontroller.io/dc/dc/commit/9546fcd6312f3e81f746ef6e32ef398810ed434a))
|
||||
* **client:** clear angular build cache on font strip to avoid stale dist ([503cb08](https://git.datacontroller.io/dc/dc/commit/503cb08b2fa40397434189f9c20eff3358eb7010))
|
||||
* **client:** postinstall removal of Metropolis [@font-face](https://git.datacontroller.io/font-face) from @clr/ui ([e6397ce](https://git.datacontroller.io/dc/dc/commit/e6397cecc13afe2a9238bdfb2b4b9b81f38d055c))
|
||||
* **client:** serve text-security-disc font locally ([80ce80e](https://git.datacontroller.io/dc/dc/commit/80ce80ece40012e59c7cd0340b4aa9a9aca46443))
|
||||
* **editor:** preserve numeric type for SAS num cols with static SOFTSELECT/HARDSELECT ([05a3289](https://git.datacontroller.io/dc/dc/commit/05a328976ea3d1d6ef7559850369aa580f0d067f))
|
||||
|
||||
## [7.7.1](https://git.datacontroller.io/dc/dc/compare/v7.7.0...v7.7.1) (2026-05-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client:** bump adapter ([d26f7d2](https://git.datacontroller.io/dc/dc/commit/d26f7d2511008634124c7d6fde115abb43db9c43))
|
||||
* **sas:** bump cli ([d60029d](https://git.datacontroller.io/dc/dc/commit/d60029deae0ec21f3b8570461e2a4ca041d58f72))
|
||||
|
||||
# [7.7.0](https://git.datacontroller.io/dc/dc/compare/v7.6.0...v7.7.0) (2026-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump adapter to 4.16.6 ([1707f38](https://git.datacontroller.io/dc/dc/commit/1707f3802a97de8c659f1a88c92fc917e8a30615))
|
||||
* remove data:image/svg+xml CSP violation, use class instead changing style directly ([d66eb5d](https://git.datacontroller.io/dc/dc/commit/d66eb5dfc2dbb01f1e6c0c7d15fc2ad2a39dd829))
|
||||
* remove WORK, SASUSER and CASUSER as library options. [#224](https://git.datacontroller.io/dc/dc/issues/224) ([ec66631](https://git.datacontroller.io/dc/dc/commit/ec66631a33aabb8ab2f92fe22c15440127085782))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* auto-save CAS tables [#224](https://git.datacontroller.io/dc/dc/issues/224) ([40d04a5](https://git.datacontroller.io/dc/dc/commit/40d04a53c4c00183116bdbd08397e0f2ffb1f578))
|
||||
* autoload CAS tables. [#224](https://git.datacontroller.io/dc/dc/issues/224) ([d5ebb01](https://git.datacontroller.io/dc/dc/commit/d5ebb01ce381f5f4ec06de041f3ab9e632c02e43))
|
||||
|
||||
# [7.6.0](https://git.datacontroller.io/dc/dc/compare/v7.5.0...v7.6.0) (2026-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add label and tooltip for libref download, sanitise input ([52d5803](https://git.datacontroller.io/dc/dc/commit/52d58036a40e25847e900f9b04a77dbcc409c12b))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* configurable email alerts. Closes [#217](https://git.datacontroller.io/dc/dc/issues/217) ([2ccf0d1](https://git.datacontroller.io/dc/dc/commit/2ccf0d11000129629a0665421135b7530af9892f))
|
||||
|
||||
# [7.5.0](https://git.datacontroller.io/dc/dc/compare/v7.4.1...v7.5.0) (2026-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add workflow audits, update deps ([66e98a9](https://git.datacontroller.io/dc/dc/commit/66e98a96cbd092e762b94a04660f8e17ca003ceb))
|
||||
* allow CSV uploads with licence row limit ([5b260e4](https://git.datacontroller.io/dc/dc/commit/5b260e49153dd85bc0023ad94d8a5f57b8ffa6dc)), closes [#213](https://git.datacontroller.io/dc/dc/issues/213)
|
||||
* bumping cli and pinning versions in .npmrc ([80039f4](https://git.datacontroller.io/dc/dc/commit/80039f4876c8e09dc477678e1eff58329094c9e9))
|
||||
* guard CSV upload with fileUpload licence flag ([ed40df6](https://git.datacontroller.io/dc/dc/commit/ed40df62953c3055770b5cbf50738f4a48b943cd))
|
||||
* parse embed param from window.location.hash for hash router compatibility ([0269c24](https://git.datacontroller.io/dc/dc/commit/0269c2421db245f7f5405678605cb4d4587e2a67))
|
||||
* quote CSV char values. Closes [#215](https://git.datacontroller.io/dc/dc/issues/215) ([d9980e8](https://git.datacontroller.io/dc/dc/commit/d9980e866d1a2fe7a731ff279d73accd35003e67))
|
||||
* resolve outer promise in parseCsvFile for non-WLATIN1 path ([4ee15e1](https://git.datacontroller.io/dc/dc/commit/4ee15e1b6e83f27f279fc345e6998452a8f64d7e))
|
||||
* use XLSX for CSV row truncation to handle new lines in values ([6d590c0](https://git.datacontroller.io/dc/dc/commit/6d590c050dcd593a73464fae5604f774f016b10d))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add embed URL parameter to hide header and back button ([b0dc441](https://git.datacontroller.io/dc/dc/commit/b0dc441d681369e06eee58288dbdbb236f930bdc)), closes [#214](https://git.datacontroller.io/dc/dc/issues/214)
|
||||
* add target libref input to config download ([a89657b](https://git.datacontroller.io/dc/dc/commit/a89657b0b81b9c531f64c0dda2714b4eb16c4bc9)), closes [#212](https://git.datacontroller.io/dc/dc/issues/212)
|
||||
* export config service to allow dclib swapping. Closes [#212](https://git.datacontroller.io/dc/dc/issues/212) ([326c26f](https://git.datacontroller.io/dc/dc/commit/326c26fddfa88a0dc4ca79d3bd0c77c4d807f37c))
|
||||
|
||||
## [7.4.1](https://git.datacontroller.io/dc/dc/compare/v7.4.0...v7.4.1) (2026-03-12)
|
||||
|
||||
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "src/images",
|
||||
"output": "images"
|
||||
"output": "images",
|
||||
"ignore": ["spinner.svg", "caret.svg"]
|
||||
}
|
||||
],
|
||||
"styles": ["src/styles.scss"],
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { defineConfig } from "cypress";
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
reporter: "mochawesome",
|
||||
reporter: 'mochawesome',
|
||||
|
||||
reporterOptions: {
|
||||
reportDir: "cypress/results",
|
||||
reportDir: 'cypress/results',
|
||||
overwrite: false,
|
||||
html: true,
|
||||
json: false,
|
||||
json: false
|
||||
},
|
||||
viewportHeight: 900,
|
||||
viewportWidth: 1600,
|
||||
@@ -16,24 +16,25 @@ export default defineConfig({
|
||||
defaultCommandTimeout: 30000,
|
||||
|
||||
env: {
|
||||
hosturl: "http://localhost:4200",
|
||||
appLocation: "",
|
||||
site_id_SAS9: "70221618",
|
||||
site_id_SASVIYA: "70253615",
|
||||
site_id_SASJS: "123",
|
||||
serverType: "SASJS",
|
||||
libraryToOpenIncludes_SASVIYA: "viya",
|
||||
libraryToOpenIncludes_SAS9: "dc",
|
||||
libraryToOpenIncludes_SASJS: "dc",
|
||||
hosturl: 'http://localhost:4200',
|
||||
appLocation: '',
|
||||
site_id_SAS9: '70221618',
|
||||
site_id_SASVIYA: '70253615',
|
||||
site_id_SASJS: '123',
|
||||
serverType: 'SASJS',
|
||||
libraryToOpenIncludes_SASVIYA: 'viya',
|
||||
libraryToOpenIncludes_SAS9: 'dc',
|
||||
libraryToOpenIncludes_SASJS: 'dc',
|
||||
debug: false,
|
||||
screenshotOnRunFailure: false,
|
||||
longerCommandTimeout: 50000,
|
||||
testLicenceUserLimits: false,
|
||||
testLicenceUserLimits: false
|
||||
},
|
||||
|
||||
e2e: {
|
||||
video: true,
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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;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) {
|
||||
|
||||
@@ -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",
|
||||
"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",
|
||||
@@ -37,21 +37,21 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.2.18",
|
||||
"@angular/animations": "^19.2.20",
|
||||
"@angular/cdk": "^19.2.19",
|
||||
"@angular/common": "^19.2.18",
|
||||
"@angular/compiler": "^19.2.18",
|
||||
"@angular/core": "^19.2.18",
|
||||
"@angular/forms": "^19.2.18",
|
||||
"@angular/platform-browser": "^19.2.18",
|
||||
"@angular/platform-browser-dynamic": "^19.2.18",
|
||||
"@angular/router": "^19.2.18",
|
||||
"@angular/common": "^19.2.20",
|
||||
"@angular/compiler": "^19.2.20",
|
||||
"@angular/core": "^19.2.20",
|
||||
"@angular/forms": "^19.2.20",
|
||||
"@angular/platform-browser": "^19.2.20",
|
||||
"@angular/platform-browser-dynamic": "^19.2.20",
|
||||
"@angular/router": "^19.2.20",
|
||||
"@cds/core": "^6.15.1",
|
||||
"@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",
|
||||
"@sasjs/adapter": "^4.16.3",
|
||||
"@handsontable/angular-wrapper": "^17.1.0",
|
||||
"@sasjs/adapter": "^4.17.0",
|
||||
"@sasjs/utils": "^3.5.3",
|
||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||
"@types/d3-graphviz": "^2.6.7",
|
||||
@@ -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",
|
||||
@@ -86,18 +86,18 @@
|
||||
"zone.js": "~0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.2.19",
|
||||
"@angular-devkit/build-angular": "^19.2.24",
|
||||
"@angular-eslint/builder": "19.8.1",
|
||||
"@angular-eslint/eslint-plugin": "19.8.1",
|
||||
"@angular-eslint/eslint-plugin-template": "19.8.1",
|
||||
"@angular-eslint/schematics": "19.8.1",
|
||||
"@angular-eslint/template-parser": "19.8.1",
|
||||
"@angular/cli": "^19.2.19",
|
||||
"@angular/compiler-cli": "^19.2.18",
|
||||
"@angular/cli": "^19.2.24",
|
||||
"@angular/compiler-cli": "^19.2.20",
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@compodoc/compodoc": "^1.1.21",
|
||||
"@compodoc/compodoc": "^1.2.1",
|
||||
"@cypress/webpack-preprocessor": "^5.17.1",
|
||||
"@lhci/cli": "^0.12.0",
|
||||
"@lhci/cli": "^0.15.1",
|
||||
"@types/core-js": "^2.5.5",
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/es6-shim": "^0.31.39",
|
||||
@@ -105,15 +105,15 @@
|
||||
"@types/lodash-es": "^4.17.3",
|
||||
"@types/marked": "^4.3.0",
|
||||
"@types/node": "12.20.50",
|
||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||
"@typescript-eslint/parser": "^5.29.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.1",
|
||||
"@typescript-eslint/parser": "8.31.1",
|
||||
"core-js": "^2.5.4",
|
||||
"cypress": "12.17.1",
|
||||
"cypress": "^15.14.2",
|
||||
"cypress-file-upload": "^5.0.8",
|
||||
"cypress-plugin-tab": "^1.0.5",
|
||||
"cypress-real-events": "^1.8.1",
|
||||
"es6-shim": "^0.35.5",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint": "8.57.1",
|
||||
"git-describe": "^4.0.4",
|
||||
"jasmine-core": "~5.1.2",
|
||||
"karma": "~6.4.3",
|
||||
@@ -132,5 +132,8 @@
|
||||
"typescript": "~5.8.3",
|
||||
"wait-on": "^6.0.1",
|
||||
"watch": "^1.0.2"
|
||||
},
|
||||
"overrides": {
|
||||
"ajv": "8.18.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
@@ -0,0 +1,59 @@
|
||||
import { readFileSync, writeFileSync, statSync, rmSync, existsSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
|
||||
/**
|
||||
* Remove Clarity's Metropolis @font-face blocks from clr-ui.min.css.
|
||||
*
|
||||
* Why: Clarity ships Metropolis as base64 data: URLs. The deployed app
|
||||
* runs under CSP `default-src 'self'` (no data: font-src), so every page
|
||||
* logs a font-load failure for each weight. Firefox preemptively
|
||||
* validates every parsed src against CSP even when a later @font-face
|
||||
* supersedes the rule at render time, so the only way to silence the
|
||||
* console is to remove the offending blocks from the parsed CSS.
|
||||
*
|
||||
* Our styles.scss declares the same family/weight/style with same-origin
|
||||
* .woff files, so removing Clarity's blocks entirely is safe and leaves
|
||||
* Metropolis fully functional.
|
||||
*
|
||||
* Idempotent: matches by font-family, so works on a fresh install or a
|
||||
* file that's already been stripped on a previous run.
|
||||
*/
|
||||
const target = resolve('node_modules/@clr/ui/clr-ui.min.css')
|
||||
|
||||
let css
|
||||
try {
|
||||
css = readFileSync(target, 'utf8')
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log(`skip: ${target} not found (likely pre-install run)`)
|
||||
process.exit(0)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
const sizeBefore = statSync(target).size
|
||||
const blockRe = /@font-face\{[^}]*Metropolis[^}]*\}/g
|
||||
const matches = css.match(blockRe) ?? []
|
||||
|
||||
if (matches.length === 0) {
|
||||
console.log(`already stripped: ${target}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const stripped = css.replace(blockRe, '')
|
||||
writeFileSync(target, stripped)
|
||||
const sizeAfter = Buffer.byteLength(stripped)
|
||||
console.log(
|
||||
`removed ${matches.length} Metropolis @font-face block(s) from clr-ui.min.css ` +
|
||||
`(${sizeBefore} -> ${sizeAfter} bytes, saved ${sizeBefore - sizeAfter})`
|
||||
)
|
||||
|
||||
// Webpack 5's persistent cache treats node_modules as immutable
|
||||
// (snapshot.module.managedPaths default), so in-place edits don't
|
||||
// invalidate cached entries. Drop the Angular build cache so the next
|
||||
// build re-reads our stripped clr-ui.min.css.
|
||||
const cacheDir = resolve('.angular/cache')
|
||||
if (existsSync(cacheDir)) {
|
||||
rmSync(cacheDir, { recursive: true, force: true })
|
||||
console.log(`cleared ${cacheDir} (webpack persistent cache)`)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -209,6 +209,7 @@ export class AppComponent {
|
||||
dcPath: getAppAttribute('dcPath') || '',
|
||||
debug: getAppAttribute('debug') === 'true' || false,
|
||||
useComputeApi: this.parseComputeApi(getAppAttribute('useComputeApi')),
|
||||
runAsTask: getAppAttribute('runAsTask') === 'true' || false,
|
||||
contextName: getAppAttribute('contextName') || '',
|
||||
hotLicenceKey: getAppAttribute('hotLicenceKey') || ''
|
||||
}
|
||||
|
||||
@@ -373,7 +373,7 @@ export class AutomaticComponent implements OnInit {
|
||||
let contextname = `&_contextname=${params.contextName}`
|
||||
let admin = `&admin=${params.admin}`
|
||||
let dcPath = `&dcpath=${params.dcPath}`
|
||||
let debug = `&_debug=131`
|
||||
let debug = this.sasService.getDebugUrlParam()
|
||||
|
||||
let programUrl =
|
||||
serverUrl +
|
||||
|
||||
@@ -251,7 +251,7 @@ export class ManualComponent implements OnInit {
|
||||
this.selectedAdminGroup +
|
||||
'&DCPATH=' +
|
||||
this.dcPath +
|
||||
'&_debug=131'
|
||||
this.sasService.getDebugUrlParam()
|
||||
|
||||
window.open(url, '_blank')
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import Handsontable from 'handsontable'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { sanitiseForSas } from '../shared/utils/sanitise'
|
||||
import { SasStoreService } from '../services/sas-store.service'
|
||||
|
||||
type AOA = any[][]
|
||||
@@ -43,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'
|
||||
@@ -132,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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,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
|
||||
@@ -962,6 +988,8 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
cancelEdit() {
|
||||
this.cancelBulkValidation({ revert: false })
|
||||
|
||||
this.toggleHotPlugin('contextMenu', false)
|
||||
|
||||
this.cellValidationSource = []
|
||||
@@ -991,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)
|
||||
@@ -1000,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) {
|
||||
@@ -1669,7 +1852,7 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.submit = true
|
||||
const updateParams: any = {}
|
||||
updateParams.ACTION = 'LOAD'
|
||||
this.message = this.message.replace(/\n/g, '. ')
|
||||
this.message = sanitiseForSas(this.message.replace(/\n/g, '. '))
|
||||
updateParams.MESSAGE = this.message
|
||||
// updateParams.APPROVER = this.approver;
|
||||
updateParams.LIBDS = this.libds
|
||||
@@ -1968,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
|
||||
@@ -2000,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
|
||||
@@ -2075,14 +2282,12 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
cellHadSource && cellHasValue
|
||||
)
|
||||
|
||||
hot.render()
|
||||
if (!skipRender) hot.render()
|
||||
})
|
||||
}
|
||||
|
||||
} else if (validationSourceIndex < 0) {
|
||||
/**
|
||||
* Send request to sas.
|
||||
*/
|
||||
if (validationSourceIndex < 0) {
|
||||
const data = {
|
||||
SASControlTable: [
|
||||
{
|
||||
@@ -2114,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
|
||||
@@ -2140,48 +2377,36 @@ 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
|
||||
*/
|
||||
|
||||
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()
|
||||
|
||||
if (!skipRender) {
|
||||
hot.render()
|
||||
hot.validateRows([row])
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
//Removing the spinner from cell, so validation not fail
|
||||
hot.setCellMeta(row, column, 'renderer', noSpinnerRenderer)
|
||||
this.currentEditRecordLoadings.splice(
|
||||
this.currentEditRecordLoadings.indexOf(column),
|
||||
1
|
||||
resolve()
|
||||
}, 100)
|
||||
)
|
||||
hot.deselectCell()
|
||||
} 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
|
||||
}
|
||||
|
||||
/**
|
||||
* If hash table limit reached, remove the oldest element.
|
||||
@@ -2198,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)
|
||||
|
||||
if (myEpoch !== this.validationEpoch) return
|
||||
|
||||
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(
|
||||
@@ -2222,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(() => {
|
||||
if (!skipRender) {
|
||||
hot.setCellMeta(row, column, 'renderer', noSpinnerRenderer)
|
||||
hot.render()
|
||||
}
|
||||
|
||||
//Remove error icon on the edit record modal field
|
||||
this.currentEditRecordErrors.splice(
|
||||
@@ -2236,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() {
|
||||
@@ -2937,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
|
||||
@@ -2955,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')
|
||||
|
||||
@@ -3019,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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { sanitiseForSas } from '../../shared/utils/sanitise'
|
||||
import { SasStoreService } from '../../services/sas-store.service'
|
||||
import {
|
||||
Component,
|
||||
@@ -136,7 +137,7 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
|
||||
|
||||
public async rejecting() {
|
||||
this.rejectLoading = true
|
||||
this.submitReason = this.submitReason.replace(/\n/g, '. ')
|
||||
this.submitReason = sanitiseForSas(this.submitReason.replace(/\n/g, '. '))
|
||||
|
||||
let rejParams = {
|
||||
STP_ACTION: 'REJECT_TABLE',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -641,6 +641,23 @@ export class SasService {
|
||||
this.sasjsAdapter.setDebugState(state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `&_debug=...` URL segment honoring the live adapter
|
||||
* config. Empty string when debug is off. `128` on the Viya WEB JES path
|
||||
* with `runAsTask` enabled, `131` otherwise.
|
||||
*/
|
||||
public getDebugUrlParam(): string {
|
||||
const config = this.sasjsAdapter.getSasjsConfig()
|
||||
if (!config.debug) return ''
|
||||
const value =
|
||||
config.serverType === ServerType.SasViya &&
|
||||
config.useComputeApi === null &&
|
||||
config.runAsTask === true
|
||||
? 128
|
||||
: 131
|
||||
return `&_debug=${value}`
|
||||
}
|
||||
|
||||
public getSasjsInstance() {
|
||||
return this.sasjsAdapter
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -284,10 +304,18 @@ export class DcValidator {
|
||||
)
|
||||
|
||||
if (source.length > 0) {
|
||||
// For SAS num cols keep type='numeric' so HOT's numericEditor /
|
||||
// numericRenderer + numbro coercion stay alive — same per-column
|
||||
// model as the per-cell pattern in
|
||||
// editor.component.ts:reSetCellValidationValues().
|
||||
this.rules[i].source = source
|
||||
this.rules[i].type = 'autocomplete'
|
||||
this.rules[i].editor = 'autocomplete.custom'
|
||||
this.rules[i].renderer = 'autocomplete'
|
||||
this.rules[i].filter = false
|
||||
|
||||
if (this.rules[i].sasType !== 'num') {
|
||||
this.rules[i].type = 'autocomplete'
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hasDqRules(ruleColName, ['SOFTSELECT'])) {
|
||||
@@ -309,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.
|
||||
|
||||
@@ -10,6 +10,9 @@ export interface DcColumnSettings {
|
||||
valid?: boolean
|
||||
desc?: string
|
||||
clsRule?: string
|
||||
// SAS-side column type from $dataFormats (e.g. 'num', 'char') — distinct
|
||||
// from Handsontable's `type` which drives renderer/editor selection
|
||||
sasType?: string
|
||||
}
|
||||
|
||||
export interface DcValidation extends HotColumnSettings, DcColumnSettings {}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -38,11 +38,52 @@ describe('DC Validator - merge spec rules', () => {
|
||||
data: 'test_col',
|
||||
desc: 'test_desc',
|
||||
clsRule: 'cls_rule',
|
||||
length: 8
|
||||
length: 8,
|
||||
sasType: 'test_type'
|
||||
}
|
||||
]
|
||||
|
||||
expect(mergeColsRules(cols, rules, $dataFormats)).toEqual(expected)
|
||||
expect(cols[0].TYPE).toEqual('test_type')
|
||||
})
|
||||
|
||||
it('should populate sasType for num and char cols', () => {
|
||||
const rules: DcValidation[] = [{ data: 'num_col' }, { data: 'char_col' }]
|
||||
const cols: Col[] = [
|
||||
{
|
||||
NAME: 'num_col',
|
||||
MEMLABEL: '',
|
||||
DESC: '',
|
||||
LONGDESC: '',
|
||||
TYPE: '',
|
||||
CLS_RULE: '',
|
||||
VARNUM: 0,
|
||||
LABEL: '',
|
||||
FMTNAME: '',
|
||||
DDTYPE: ''
|
||||
},
|
||||
{
|
||||
NAME: 'char_col',
|
||||
MEMLABEL: '',
|
||||
DESC: '',
|
||||
LONGDESC: '',
|
||||
TYPE: '',
|
||||
CLS_RULE: '',
|
||||
VARNUM: 0,
|
||||
LABEL: '',
|
||||
FMTNAME: '',
|
||||
DDTYPE: ''
|
||||
}
|
||||
]
|
||||
const $dataFormats: any = {
|
||||
vars: {
|
||||
num_col: { format: 'best.', label: '', length: '8', type: 'num' },
|
||||
char_col: { format: '$32.', label: '', length: '32', type: 'char' }
|
||||
}
|
||||
}
|
||||
|
||||
const merged = mergeColsRules(cols, rules, $dataFormats)
|
||||
expect(merged.find((r) => r.data === 'num_col')?.sasType).toEqual('num')
|
||||
expect(merged.find((r) => r.data === 'char_col')?.sasType).toEqual('char')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export const mergeColsRules = (
|
||||
if (rule && col.DESC) rule.desc = col.DESC
|
||||
if (rule && colFormats.length) rule.length = parseInt(colFormats.length)
|
||||
if (rule && col.CLS_RULE) rule.clsRule = col.CLS_RULE
|
||||
if (rule && colFormats?.type) rule.sasType = colFormats.type
|
||||
}
|
||||
|
||||
return rules
|
||||
|
||||
@@ -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]
|
||||
})
|
||||
|
||||
@@ -80,15 +80,13 @@ export class SidebarComponent implements OnInit {
|
||||
public resizeStart() {
|
||||
this.resizing = true
|
||||
|
||||
let body = document.getElementsByTagName('body')[0]
|
||||
body.style.cssText = 'user-select: none'
|
||||
document.body.classList.add('select-none')
|
||||
}
|
||||
|
||||
public resizeEnd() {
|
||||
this.resizing = false
|
||||
|
||||
let body = document.getElementsByTagName('body')[0]
|
||||
body.style.cssText = ''
|
||||
document.body.classList.remove('select-none')
|
||||
}
|
||||
|
||||
@HostListener('document:mousemove', ['$event'])
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Strips characters that could cause SAS macro injection (& % ;).
|
||||
*/
|
||||
export function sanitiseForSas(input: string): string {
|
||||
return input.replace(/[%&;]/g, '')
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const routes: Routes = [{ path: ':tableId', component: StageComponent }]
|
||||
CommonModule,
|
||||
ClarityModule,
|
||||
RouterModule.forChild(routes),
|
||||
HotTableModule.forRoot()
|
||||
HotTableModule
|
||||
]
|
||||
})
|
||||
export class StageModule {}
|
||||
|
||||
@@ -236,14 +236,31 @@
|
||||
<div class="admin-action">
|
||||
Download Configuration
|
||||
|
||||
<div class="libref-group">
|
||||
<clr-tooltip class="libref-tooltip">
|
||||
<label clrTooltipTrigger class="libref-label">
|
||||
Target DC Library
|
||||
<cds-icon shape="info-circle" size="16"></cds-icon>
|
||||
</label>
|
||||
<clr-tooltip-content
|
||||
clrPosition="bottom-left"
|
||||
clrSize="md"
|
||||
*clrIfOpen
|
||||
>
|
||||
Enter the target DC library and the downloaded files will
|
||||
contain this, instead of the original.
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="clr-input libref-input"
|
||||
maxlength="8"
|
||||
[ngModel]="dcLib"
|
||||
(ngModelChange)="targetLibref = $event.toUpperCase()"
|
||||
placeholder="Target Libref"
|
||||
placeholder="e.g. MYLIB"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
(click)="downloadConfiguration()"
|
||||
[disabled]="targetLibref !== dcLib && !isValidLibref(targetLibref)"
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
.libref-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.libref-label {
|
||||
cursor: pointer;
|
||||
font-size: 0.55rem;
|
||||
font-weight: 600;
|
||||
color: var(--clr-p4-color, #565656);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.libref-input {
|
||||
width: 100px;
|
||||
margin: 0 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@@ -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,9 +774,6 @@ 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) => {
|
||||
@@ -780,7 +782,6 @@ export class ViewerComponent
|
||||
.catch((err: any) => {
|
||||
this.loggerService.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
this.lib = ds[0]
|
||||
|
||||
@@ -814,9 +815,6 @@ export class ViewerComponent
|
||||
|
||||
libDataset = this.libDataset
|
||||
this.libTab = libDataset
|
||||
} else {
|
||||
if (globals.viewer.startupSet) {
|
||||
this.libraries = globals.viewer.libraries
|
||||
} else {
|
||||
await this.sasStoreService
|
||||
.viewLibs()
|
||||
@@ -826,7 +824,6 @@ export class ViewerComponent
|
||||
.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
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -49,7 +49,8 @@
|
||||
serverType="SASJS"
|
||||
loginMechanism="Redirected"
|
||||
debug="false"
|
||||
useComputeApi="true"
|
||||
useComputeApi="null"
|
||||
runAsTask="true"
|
||||
contextName="SAS Job Execution compute context"
|
||||
adminGroup="SASAdministrators"
|
||||
dcPath="/tmp/dc"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -9,9 +12,39 @@
|
||||
|
||||
@import './colors.scss';
|
||||
|
||||
/* CSP: replace Clarity's base64 Metropolis @font-face srcs with same-origin files. */
|
||||
@font-face {
|
||||
font-family: 'Metropolis';
|
||||
src: url('./assets/fonts/Metropolis-200.woff') format('woff');
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Metropolis';
|
||||
src: url('./assets/fonts/Metropolis-400.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Metropolis';
|
||||
src: url('./assets/fonts/Metropolis-500.woff') format('woff');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Metropolis';
|
||||
src: url('./assets/fonts/Metropolis-600.woff') format('woff');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: text-security-disc;
|
||||
src: url('https://raw.githubusercontent.com/noppa/text-security/master/dist/text-security-disc.woff');
|
||||
src: url('./assets/fonts/text-security-disc.woff') format('woff');
|
||||
}
|
||||
|
||||
// TODO: IMPORTANT CSP WOKRAROUND
|
||||
@@ -509,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 {
|
||||
@@ -597,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;
|
||||
}
|
||||
@@ -1882,19 +1955,6 @@ app-query {
|
||||
}
|
||||
}
|
||||
|
||||
.clause-row:after {
|
||||
position: relative;
|
||||
content: "";
|
||||
height: .41667rem;
|
||||
width: .41667rem;
|
||||
top: .29167rem;
|
||||
right: .25rem;
|
||||
background-image: url(data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org…%2C9.84%2C3.24a0.68%2C0.68%2C0%2C1%2C1%2C1%2C1Z%22%2F%3E%0A%3C%2Fsvg%3E%0A);
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre[class*="language-"] {
|
||||
padding: 8px;
|
||||
@@ -4767,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 {
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
{
|
||||
"name": "dcfrontend",
|
||||
"version": "7.4.1",
|
||||
"version": "7.8.2",
|
||||
"description": "Data Controller",
|
||||
"devDependencies": {
|
||||
"@saithodev/semantic-release-gitea": "^2.1.0",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/commit-analyzer": "^10.0.1",
|
||||
"@semantic-release/commit-analyzer": "13.0.1",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/npm": "11.0.0",
|
||||
"@semantic-release/release-notes-generator": "^11.0.4",
|
||||
"commit-and-tag-version": "^11.2.2",
|
||||
"@semantic-release/npm": "13.1.5",
|
||||
"@semantic-release/release-notes-generator": "14.1.0",
|
||||
"commit-and-tag-version": "12.7.1",
|
||||
"prettier": "^3.7.4"
|
||||
},
|
||||
"overrides": {
|
||||
"got": "11.8.6"
|
||||
},
|
||||
"scripts": {
|
||||
"install": "cd client && npm i && cd ../sas && npm i",
|
||||
"build-frontend": "cd client && npm run build",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ignore-scripts=true
|
||||
save-exact=true
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"fromjs": [
|
||||
{
|
||||
"ADMIN": "DCDEFAULT",
|
||||
"ADMIN": "AllUsers",
|
||||
"DCPATH": "/tmp/dcdata"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"": {
|
||||
"name": "dc-sas",
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "^4.15.0",
|
||||
"@sasjs/core": "^4.62.0"
|
||||
"@sasjs/cli": "4.17.4",
|
||||
"@sasjs/core": "4.67.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
@@ -124,7 +124,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -147,7 +146,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -202,13 +200,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/adapter": {
|
||||
"version": "4.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.16.3.tgz",
|
||||
"integrity": "sha512-xcoZT9qZhF6pXvXx4bHxbmauLdEHng8pSlTK4F6asUkHNR5uzeSvY6znA1yJqK+8FFtsVILyvMQyGyhWw6WsOA==",
|
||||
"version": "4.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.16.7.tgz",
|
||||
"integrity": "sha512-pOKAhOPijr663PFWGx+JEcWaMOGwICMA0w8LeXHPHQwvbHVGPCJKV5QroGqrjTO62HlhsHzow4Vb+DjIui1jgQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "3.5.6",
|
||||
"axios": "^1.13.5",
|
||||
"@sasjs/utils": "^3.5.6",
|
||||
"axios": "1.16.0",
|
||||
"axios-cookiejar-support": "5.0.5",
|
||||
"form-data": "4.0.4",
|
||||
"https": "1.0.0",
|
||||
@@ -216,9 +214,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/adapter/node_modules/@sasjs/utils": {
|
||||
"version": "3.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.5.6.tgz",
|
||||
"integrity": "sha512-jx8zWSOysDD66vTjA0BWiZ8bcFqmqh8F+56fUCgLmJhm89eDbKrGF3mDKMQx3UE7d2+gxp9xYhJCdaBWz0Dlxw==",
|
||||
"version": "3.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.5.8.tgz",
|
||||
"integrity": "sha512-rR2nCJG5AsuLj+nonpVO4PvA5OW8pUhDivY/25E4PCrQYLAmOaWQyIRne1gvSLL5ELzsOwRZWz6Zf6a02uNayA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fast-csv/format": "4.3.5",
|
||||
@@ -251,15 +249,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/cli": {
|
||||
"version": "4.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.15.0.tgz",
|
||||
"integrity": "sha512-lVKzm8+4b9VSqbchfLnzyNm53cKDZfqSM8KU7izD/JDBsuATSZtjLo61iNenZaPg9d3WXglb1jf2ASbVKbXxUQ==",
|
||||
"version": "4.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.17.4.tgz",
|
||||
"integrity": "sha512-lNkIy6sojgT5mRQ/tkh8lyrIxNhrREw+CTkfEmy+mDiOh9COfGWMoSpBjRn2iqHX+dwOKaUlq6UwU2zHDLOmqQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "4.16.3",
|
||||
"@sasjs/core": "4.62.0",
|
||||
"@sasjs/adapter": "4.16.7",
|
||||
"@sasjs/core": "4.67.1",
|
||||
"@sasjs/lint": "2.4.3",
|
||||
"@sasjs/utils": "3.5.6",
|
||||
"@sasjs/utils": "3.5.8",
|
||||
"adm-zip": "0.5.10",
|
||||
"chalk": "4.1.2",
|
||||
"dotenv": "16.0.3",
|
||||
@@ -282,9 +280,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/cli/node_modules/@sasjs/utils": {
|
||||
"version": "3.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.5.6.tgz",
|
||||
"integrity": "sha512-jx8zWSOysDD66vTjA0BWiZ8bcFqmqh8F+56fUCgLmJhm89eDbKrGF3mDKMQx3UE7d2+gxp9xYhJCdaBWz0Dlxw==",
|
||||
"version": "3.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.5.8.tgz",
|
||||
"integrity": "sha512-rR2nCJG5AsuLj+nonpVO4PvA5OW8pUhDivY/25E4PCrQYLAmOaWQyIRne1gvSLL5ELzsOwRZWz6Zf6a02uNayA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fast-csv/format": "4.3.5",
|
||||
@@ -317,9 +315,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/core": {
|
||||
"version": "4.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.62.0.tgz",
|
||||
"integrity": "sha512-xMWeZbxlvuCP0B9fnSTgSFbSiA0hiKDpTua8wb0ghMUOl+dnh/XF+BYgrHhWhPL9j0+k5d8mJejLmf/l/txpzg==",
|
||||
"version": "4.67.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.67.1.tgz",
|
||||
"integrity": "sha512-yb4xW8JxsWWY3ZTN4yj/mP8ZasPIHSQVjHESLui3xS0HNatBw96H9ZYGJO3LcrkDx+X5xJz45f6sdHAJYxVeSA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sasjs/lint": {
|
||||
@@ -462,14 +460,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
|
||||
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-cookiejar-support": {
|
||||
@@ -1013,9 +1011,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -1845,10 +1843,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.15.0",
|
||||
@@ -2168,7 +2169,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"psl": "^1.1.33",
|
||||
"punycode": "^2.1.1",
|
||||
@@ -2340,9 +2340,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"version": "8.20.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
||||
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "^4.15.0",
|
||||
"@sasjs/core": "^4.62.0"
|
||||
"@sasjs/cli": "4.17.4",
|
||||
"@sasjs/core": "4.67.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
@file
|
||||
@brief migration script to move from v7.0 to v7.6 of data controller
|
||||
|
||||
OPTIONAL CHANGE - upload additional data as placeholders for modifying the
|
||||
default email message
|
||||
|
||||
**/
|
||||
|
||||
%let dclib=YOURDCLIB;
|
||||
|
||||
libname &dclib "/YOUR/DATACONTROLLER/LIBRARY/PATH";
|
||||
|
||||
proc sql;
|
||||
insert into &dclib..mpe_config set
|
||||
tx_from=%sysfunc(datetime())
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
,var_scope="DC_EMAIL"
|
||||
,var_name="SUBMITTED_TEMPLATE"
|
||||
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
|
||||
!!' &alert_lib..&alert_ds has been proposed by &from_user on the '
|
||||
!!'&syshostname SAS server.'!!'0A'x!!'Reason provided: '
|
||||
!!'%superq(SUBMITTED_TXT)'
|
||||
!!'0A'x!!'This is an automated email by Data Controller for SAS. For '
|
||||
!!'documentation, please visit https://docs.datacontroller.io'
|
||||
,var_active=1
|
||||
,var_desc='Template email, sent after submitting a change';
|
||||
insert into &dclib..mpe_config set
|
||||
tx_from=%sysfunc(datetime())
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
,var_scope="DC_EMAIL"
|
||||
,var_name="APPROVED_TEMPLATE"
|
||||
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
|
||||
!!' &alert_lib..&alert_ds has been approved by &from_user on the '
|
||||
!!'&syshostname SAS server.'!!'0A'x!!'This is an automated email by Data'
|
||||
!!' Controller for SAS. For documentation, please visit '
|
||||
!!'https://docs.datacontroller.io'
|
||||
,var_active=1
|
||||
,var_desc='Template email, sent after approving a change';
|
||||
insert into &dclib..mpe_config set
|
||||
tx_from=%sysfunc(datetime())
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
,var_scope="DC_EMAIL"
|
||||
,var_name="REJECTED_TEMPLATE"
|
||||
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
|
||||
!!' &alert_lib..&alert_ds has been rejected by &from_user on the '
|
||||
!!'&syshostname SAS server.'!!'0A'x!!'Reason provided: '
|
||||
!!'%superq(REVIEW_REASON_TXT)'
|
||||
!!'0A'x!!'This is an automated email by Data Controller for SAS. For '
|
||||
!!'documentation, please visit https://docs.datacontroller.io'
|
||||
,var_active=1
|
||||
,var_desc='Template email, sent after rejecting a change';
|
||||
@@ -127,6 +127,11 @@ run;
|
||||
filename __out email (&emails)
|
||||
subject="Table &alert_lib..&alert_ds has been &alert_event";
|
||||
|
||||
data work.alertmessage;
|
||||
set &mpelib..mpe_config;
|
||||
where &dc_dttmtfmt. lt tx_to;
|
||||
where also var_scope='DC_EMAIL' and var_name="&alert_event._TEMPLATE";
|
||||
run;
|
||||
%local SUBMITTED_TXT;
|
||||
%if &alert_event=SUBMITTED %then %do;
|
||||
data _null_;
|
||||
@@ -136,30 +141,54 @@ filename __out email (&emails)
|
||||
run;
|
||||
data _null_;
|
||||
File __out lrecl=32000;
|
||||
length txt $2048;
|
||||
%if %mf_getattrn(alertmessage,NLOBS)=0 %then %do;
|
||||
put 'Dear user,';
|
||||
put ' ';
|
||||
put "Please be advised that a change to table &alert_lib..&alert_ds has "
|
||||
"been proposed by &from_user on the '&syshostname' SAS server.";
|
||||
"been proposed by &from_user on the &syshostname SAS server.";
|
||||
put " ";
|
||||
length txt $2048;
|
||||
txt=symget('SUBMITTED_TXT');
|
||||
put "Reason provided: " txt;
|
||||
put " ";
|
||||
put "This is an automated email by Data Controller for SAS. For "
|
||||
"documentation, please visit https://docs.datacontroller.io";
|
||||
%end;
|
||||
%else %do;
|
||||
/* take template from config table */
|
||||
set work.alertmessage;
|
||||
cnt=countw(var_value,'0A'x);
|
||||
do i=1 to cnt;
|
||||
txt=resolve(scan(var_value,i,'0A'x));
|
||||
put txt /;
|
||||
end;
|
||||
%end;
|
||||
run;
|
||||
%end;
|
||||
%else %if &alert_event=APPROVED %then %do;
|
||||
/* there is no approval message */
|
||||
data _null_;
|
||||
File __out lrecl=32000;
|
||||
length txt $2048;
|
||||
%if %mf_getattrn(alertmessage,NLOBS)=0 %then %do;
|
||||
/* fallback message */
|
||||
put 'Dear user,';
|
||||
put ' ';
|
||||
put "Please be advised that a change to table &alert_lib..&alert_ds has "
|
||||
"been approved by &from_user on the '&syshostname' SAS server.";
|
||||
"been approved by &from_user on the &syshostname SAS server.";
|
||||
put " ";
|
||||
put "This is an automated email by Data Controller for SAS. For "
|
||||
"documentation, please visit https://docs.datacontroller.io";
|
||||
%end;
|
||||
%else %do;
|
||||
/* take template from config table */
|
||||
set work.alertmessage;
|
||||
cnt=countw(var_value,'0A'x);
|
||||
do i=1 to cnt;
|
||||
txt=resolve(scan(var_value,i,'0A'x));
|
||||
put txt /;
|
||||
end;
|
||||
%end;
|
||||
run;
|
||||
%end;
|
||||
%else %if &alert_event=REJECTED %then %do;
|
||||
@@ -170,17 +199,29 @@ filename __out email (&emails)
|
||||
run;
|
||||
data _null_;
|
||||
File __out lrecl=32000;
|
||||
length txt $2048;
|
||||
%if %mf_getattrn(alertmessage,NLOBS)=0 %then %do;
|
||||
/* fallback message */
|
||||
put 'Dear user,';
|
||||
put ' ';
|
||||
put "Please be advised that a change to table &alert_lib..&alert_ds has "
|
||||
"been rejected by &from_user on the '&syshostname' SAS server.";
|
||||
"been rejected by &from_user on the &syshostname SAS server.";
|
||||
put " ";
|
||||
length txt $2048;
|
||||
txt=symget('REVIEW_REASON_TXT');
|
||||
put "Reason provided: " txt;
|
||||
put " ";
|
||||
put "This is an automated email by Data Controller for SAS. For "
|
||||
"documentation, please visit https://docs.datacontroller.io";
|
||||
%end;
|
||||
%else %do;
|
||||
/* take template from config table */
|
||||
set work.alertmessage;
|
||||
cnt=countw(var_value,'0A'x);
|
||||
do i=1 to cnt;
|
||||
txt=resolve(scan(var_value,i,'0A'x));
|
||||
put txt /;
|
||||
end;
|
||||
%end;
|
||||
run;
|
||||
%end;
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
from &dc_libref..mpe_submit
|
||||
where TABLE_ID="&load_ref";
|
||||
|
||||
%local base_lib base_ds;
|
||||
%let base_lib=%scan(&libds,1,.);
|
||||
%let base_ds=%scan(&libds,2,.);
|
||||
|
||||
/**
|
||||
* check if there is actually a version to restore
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
@file
|
||||
@brief Returns the path to the settings file
|
||||
@details The settings file location differs by platform:
|
||||
|
||||
@li SASJS - `<apploc>/services/public/settings.sas`
|
||||
@li SASMETA - `<apploc>/services/public/Data_Controller_Settings`
|
||||
@li SASVIYA - `<apploc>/services/settings.sas`
|
||||
|
||||
Usage:
|
||||
|
||||
%let settingspath=%mpe_getpath2settings();
|
||||
%put &=settingspath;
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mf_getapploc.sas
|
||||
@li mf_getplatform.sas
|
||||
|
||||
**/
|
||||
|
||||
%macro mpe_getpath2settings();
|
||||
%local root platform;
|
||||
%let root=%mf_getapploc();
|
||||
%let platform=%mf_getplatform();
|
||||
|
||||
%if &platform=SASJS %then %do;
|
||||
&root/services/public/settings
|
||||
%end;
|
||||
%else %if &platform=SASMETA %then %do;
|
||||
&root/services/public/Data_Controller_Settings
|
||||
%end;
|
||||
%else %if &platform=SASVIYA %then %do;
|
||||
&root/services/settings
|
||||
%end;
|
||||
|
||||
%mend mpe_getpath2settings;
|
||||
@@ -201,6 +201,44 @@ insert into &lib..mpe_config set
|
||||
,var_value=' '
|
||||
,var_active=1
|
||||
,var_desc='Activation Key';
|
||||
insert into &lib..mpe_config set
|
||||
tx_from=0
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
,var_scope="DC_EMAIL"
|
||||
,var_name="SUBMITTED_TEMPLATE"
|
||||
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
|
||||
!!' &alert_lib..&alert_ds has been proposed by &from_user on the '
|
||||
!!'&syshostname SAS server.'!!'0A'x!!'Reason provided: '
|
||||
!!'%superq(SUBMITTED_TXT)'
|
||||
!!'0A'x!!'This is an automated email by Data Controller for SAS. For '
|
||||
!!'documentation, please visit https://docs.datacontroller.io'
|
||||
,var_active=1
|
||||
,var_desc='Template email, sent after submitting a change';
|
||||
insert into &lib..mpe_config set
|
||||
tx_from=0
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
,var_scope="DC_EMAIL"
|
||||
,var_name="APPROVED_TEMPLATE"
|
||||
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
|
||||
!!' &alert_lib..&alert_ds has been approved by &from_user on the '
|
||||
!!'&syshostname SAS server.'!!'0A'x!!'This is an automated email by Data'
|
||||
!!' Controller for SAS. For documentation, please visit '
|
||||
!!'https://docs.datacontroller.io'
|
||||
,var_active=1
|
||||
,var_desc='Template email, sent after approving a change';
|
||||
insert into &lib..mpe_config set
|
||||
tx_from=0
|
||||
,tx_to='31DEC9999:23:59:59'dt
|
||||
,var_scope="DC_EMAIL"
|
||||
,var_name="REJECTED_TEMPLATE"
|
||||
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
|
||||
!!' &alert_lib..&alert_ds has been rejected by &from_user on the '
|
||||
!!'&syshostname SAS server.'!!'0A'x!!'Reason provided: '
|
||||
!!'%superq(REVIEW_REASON_TXT)'
|
||||
!!'0A'x!!'This is an automated email by Data Controller for SAS. For '
|
||||
!!'documentation, please visit https://docs.datacontroller.io'
|
||||
,var_active=1
|
||||
,var_desc='Template email, sent after rejecting a change';
|
||||
|
||||
|
||||
insert into &lib..mpe_datadictionary set
|
||||
@@ -213,7 +251,6 @@ insert into &lib..mpe_datadictionary set
|
||||
,DD_RESPONSIBLE="&sysuserid"
|
||||
,DD_SENSITIVITY="Low"
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
|
||||
insert into &lib..mpe_datadictionary set
|
||||
tx_from=0
|
||||
,DD_TYPE='TABLE'
|
||||
@@ -224,7 +261,6 @@ insert into &lib..mpe_datadictionary set
|
||||
,DD_RESPONSIBLE="&sysuserid"
|
||||
,DD_SENSITIVITY="Low"
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
|
||||
insert into &lib..mpe_datadictionary set
|
||||
tx_from=0
|
||||
,DD_TYPE='COLUMN'
|
||||
@@ -235,7 +271,6 @@ insert into &lib..mpe_datadictionary set
|
||||
,DD_RESPONSIBLE="&sysuserid"
|
||||
,DD_SENSITIVITY="Low"
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
|
||||
insert into &lib..mpe_datadictionary set
|
||||
tx_from=0
|
||||
,DD_TYPE='DIRECTORY'
|
||||
@@ -806,7 +841,7 @@ insert into &lib..mpe_selectbox set
|
||||
,select_lib="&lib"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_TYPE"
|
||||
,selectbox_value="HARDSELECT"
|
||||
,selectbox_value="NOTNULL"
|
||||
,selectbox_order=4
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
@@ -815,7 +850,7 @@ insert into &lib..mpe_selectbox set
|
||||
,select_lib="&lib"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_TYPE"
|
||||
,selectbox_value="SOFTSELECT"
|
||||
,selectbox_value="HARDSELECT"
|
||||
,selectbox_order=5
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
@@ -824,26 +859,53 @@ insert into &lib..mpe_selectbox set
|
||||
,select_lib="&lib"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_TYPE"
|
||||
,selectbox_value="NOTNULL"
|
||||
,selectbox_value="HARDSELECT_HOOK"
|
||||
,selectbox_order=6
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
selectbox_rk=%mf_increment(rk)
|
||||
,ver_from_dttm=0
|
||||
,select_lib="&lib"
|
||||
,select_ds="MPE_SECURITY"
|
||||
,base_column="DSN"
|
||||
,selectbox_value="SOME_DATASET"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_TYPE"
|
||||
,selectbox_value="SOFTSELECT"
|
||||
,selectbox_order=7
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
selectbox_rk=%mf_increment(rk)
|
||||
,ver_from_dttm=0
|
||||
,select_lib="&lib"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_TYPE"
|
||||
,selectbox_value="SOFTSELECT_HOOK"
|
||||
,selectbox_order=8
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
selectbox_rk=%mf_increment(rk)
|
||||
,ver_from_dttm=0
|
||||
,select_lib="&lib"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_ACTIVE"
|
||||
,selectbox_value="1"
|
||||
,selectbox_order=1
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
selectbox_rk=%mf_increment(rk)
|
||||
,ver_from_dttm=0
|
||||
,select_lib="&lib"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_ACTIVE"
|
||||
,selectbox_value="0"
|
||||
,selectbox_order=2
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
selectbox_rk=%mf_increment(rk)
|
||||
,ver_from_dttm=0
|
||||
,select_lib="&lib"
|
||||
,select_ds="MPE_SECURITY"
|
||||
,base_column="DSN"
|
||||
,selectbox_value="EXAMPLE"
|
||||
,selectbox_order=2
|
||||
,selectbox_value="*ALL*"
|
||||
,selectbox_order=1
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
selectbox_rk=%mf_increment(rk)
|
||||
@@ -908,24 +970,6 @@ insert into &lib..mpe_selectbox set
|
||||
,selectbox_value='AUDIT'
|
||||
,selectbox_order=4
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
selectbox_rk=%mf_increment(rk)
|
||||
,ver_from_dttm=0
|
||||
,select_lib="&lib"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_TYPE"
|
||||
,selectbox_value="HARDSELECT_HOOK"
|
||||
,selectbox_order=7
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
selectbox_rk=%mf_increment(rk)
|
||||
,ver_from_dttm=0
|
||||
,select_lib="&lib"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_TYPE"
|
||||
,selectbox_value="SOFTSELECT_HOOK"
|
||||
,selectbox_order=7
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
selectbox_rk=%mf_increment(rk)
|
||||
,ver_from_dttm=0
|
||||
|
||||
@@ -143,17 +143,17 @@
|
||||
},
|
||||
{
|
||||
"name": "vtest",
|
||||
"appLoc": "/30.SASApps/app/vtest",
|
||||
"appLoc": "/Users/&sysuserid/dctest",
|
||||
"serverType": "SASVIYA",
|
||||
"serverUrl": "https://sas.4gl.io",
|
||||
"contextName": "Datacontroller compute context",
|
||||
"contextName": "Compute Reusable",
|
||||
"testConfig": {
|
||||
"testSetUp": "sasjs/tests/testsetup.sas"
|
||||
},
|
||||
"serviceConfig": {
|
||||
"initProgram": "sasjs/utils/serviceinitviya.sas",
|
||||
"serviceFolders": [
|
||||
"sasjs/targets/viya/services_viya/usernav",
|
||||
"sasjs/targets/viya/services_viya/viya_users",
|
||||
"sasjs/targets/viya/services_viya/admin",
|
||||
"sasjs/targets/viya/services_viya/public"
|
||||
]
|
||||
@@ -166,6 +166,9 @@
|
||||
],
|
||||
"deployConfig": {
|
||||
"deployServicePack": true
|
||||
},
|
||||
"streamConfig": {
|
||||
"streamWeb": false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
<h4> SAS Macros </h4>
|
||||
@li mf_getuser.sas
|
||||
@li mf_nobs.sas
|
||||
@li mp_ds2cards.sas
|
||||
@li mp_abort.sas
|
||||
@li mp_binarycopy.sas
|
||||
@li mp_ds2cards.sas
|
||||
@li mp_ds2csv.sas
|
||||
@li mp_streamfile.sas
|
||||
@li mp_validatecol.sas
|
||||
|
||||
@author 4GL Apps Ltd
|
||||
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
|
||||
@@ -21,23 +23,33 @@
|
||||
|
||||
**/
|
||||
|
||||
%global dclib islib newlib;
|
||||
%mpeinit()
|
||||
|
||||
data _null_;
|
||||
newlib=coalescec(symget('dclib'),"&mpelib");
|
||||
%mp_validatecol(newlib,ISLIB,islib)
|
||||
call symputx('islib',islib);
|
||||
call symputx('newlib',upcase(newlib));
|
||||
putlog (_all_)(=);
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue= (&islib ne 1)
|
||||
,mac=&_program
|
||||
,msg=%nrstr(&newlib is not a valid libref)
|
||||
)
|
||||
|
||||
%let work=%sysfunc(pathname(work));
|
||||
|
||||
/* excel does not work in all envs */
|
||||
%let mime=application/vnd.ms-excel;
|
||||
%let dbms=EXCEL;
|
||||
|
||||
%let mime=application/csv;
|
||||
%let dbms=CSV;
|
||||
%let ext=csv;
|
||||
|
||||
%macro conditional_export(ds);
|
||||
%if %mf_nobs(&ds)>0 %then %do;
|
||||
PROC EXPORT DATA= &ds OUTFILE= "&work/&ds..&ext"
|
||||
DBMS=&dbms REPLACE;
|
||||
RUN;
|
||||
ods package(ProdOutput) add file="&work/&ds..&ext" mimetype="&mime";
|
||||
/* cannot use PROC EXPORT as we need to wrap all csv char values in quotes */
|
||||
/* cannot use excel as it does not work consistently in all SAS envs */
|
||||
%mp_ds2csv(&ds,outfile="&work/&newlib..&ds..csv",headerformat=NAME)
|
||||
ods package(ProdOutput) add file="&work/&newlib..&ds..&ext" mimetype="&mime";
|
||||
%end;
|
||||
%mp_abort(iftrue= (&syscc ne 0)
|
||||
,mac=&_program
|
||||
@@ -52,6 +64,7 @@ data MPE_ALERTS;
|
||||
set &mpelib..MPE_ALERTS;
|
||||
where &dc_dttmtfmt. le tx_to;
|
||||
drop tx_: ;
|
||||
if alert_lib="&mpelib" then alert_lib="&newlib";
|
||||
run;
|
||||
%conditional_export(MPE_ALERTS)
|
||||
|
||||
@@ -61,6 +74,7 @@ data MPE_COLUMN_LEVEL_SECURITY;
|
||||
where &dc_dttmtfmt. le tx_to;
|
||||
where also CLS_LIBREF ne "&mpelib";
|
||||
drop tx_: ;
|
||||
CLS_LIBREF="&newlib";
|
||||
run;
|
||||
%conditional_export(MPE_COLUMN_LEVEL_SECURITY)
|
||||
|
||||
@@ -68,6 +82,7 @@ data MPE_CONFIG;
|
||||
set &mpelib..MPE_CONFIG;
|
||||
where &dc_dttmtfmt. le tx_to;
|
||||
drop tx_: ;
|
||||
if var_name='DC_MACROS' then var_value=tranwrd(var_value,"&mpelib","&newlib");
|
||||
run;
|
||||
%conditional_export(MPE_CONFIG)
|
||||
|
||||
@@ -93,6 +108,7 @@ data MPE_EXCEL_CONFIG;
|
||||
set &mpelib..MPE_EXCEL_CONFIG;
|
||||
where &dc_dttmtfmt. le tx_to;
|
||||
drop tx_: ;
|
||||
if xl_libref="&mpelib" then xl_libref="&newlib";
|
||||
run;
|
||||
%conditional_export(MPE_EXCEL_CONFIG)
|
||||
|
||||
@@ -107,6 +123,7 @@ data MPE_ROW_LEVEL_SECURITY;
|
||||
set &mpelib..MPE_ROW_LEVEL_SECURITY;
|
||||
where &dc_dttmtfmt. le tx_to;
|
||||
drop tx_: ;
|
||||
if rls_libref="&mpelib" then rls_libref="&newlib";
|
||||
run;
|
||||
%conditional_export(MPE_ROW_LEVEL_SECURITY)
|
||||
|
||||
@@ -115,6 +132,7 @@ data MPE_SECURITY;
|
||||
set &mpelib..MPE_SECURITY;
|
||||
where &dc_dttmtfmt. le TX_TO;
|
||||
drop tx_: ;
|
||||
if libref="&mpelib" then libref="&newlib";
|
||||
run;
|
||||
%conditional_export(MPE_SECURITY)
|
||||
|
||||
@@ -142,6 +160,23 @@ data MPE_VALIDATIONS;
|
||||
run;
|
||||
%conditional_export(MPE_VALIDATIONS)
|
||||
|
||||
data MPE_XLMAP_INFO;
|
||||
set &mpelib..MPE_XLMAP_INFO;
|
||||
where &dc_dttmtfmt. le TX_TO;
|
||||
drop tx_: ;
|
||||
if XLMAP_TARGETLIBDS=:"&mpelib.." then
|
||||
XLMAP_TARGETLIBDS=tranwrd(XLMAP_TARGETLIBDS,"&mpelib..","&newlib..");
|
||||
run;
|
||||
%conditional_export(MPE_XLMAP_INFO)
|
||||
|
||||
data MPE_XLMAP_RULES;
|
||||
set &mpelib..MPE_XLMAP_RULES;
|
||||
where &dc_dttmtfmt. le TX_TO;
|
||||
drop tx_: ;
|
||||
run;
|
||||
%conditional_export(MPE_XLMAP_RULES)
|
||||
|
||||
|
||||
/* finish up zip file */
|
||||
ods package(ProdOutput) publish archive properties
|
||||
(archive_name="DCBACKUP.zip" archive_path="&work");
|
||||
|
||||
@@ -84,7 +84,8 @@ data work.reject;
|
||||
REVIEW_STATUS_ID="REJECTED";
|
||||
REVIEWED_BY_NM="&user";
|
||||
REVIEWED_ON_DTTM=&now;
|
||||
REVIEW_REASON_TXT=symget('STP_REASON');
|
||||
/* sanitise message to prevent code injection */
|
||||
REVIEW_REASON_TXT=compress(symget('STP_REASON'), '&%;');
|
||||
run;
|
||||
|
||||
%mp_lockanytable(LOCK,
|
||||
|
||||