Compare commits

..

103 Commits

Author SHA1 Message Date
sead 4ea604f9fb feat(editor): add READONLY, HIDDEN, ROUND and NUMBER_FORMAT validation rules
Build / Build-and-ng-test (pull_request) Failing after 15m15s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 20m15s
2026-06-05 21:25:13 +02:00
sead 9d97bf7ea1 fix(handsontable): restore dark mode for v17
Lighthouse Checks / lighthouse (pull_request) Failing after 17m59s
Build / Build-and-ng-test (pull_request) Failing after 17m59s
Build / Build-and-test-development (pull_request) Has been skipped
2026-06-05 14:12:11 +02:00
sead eb015d712b revert(editor): DISPLAY_VALUE change
Build / Build-and-ng-test (pull_request) Failing after 16m30s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 31m28s
Partially revert changes from 51071b463b
2026-05-26 22:08:20 +02:00
sead 1d04f4a42c refactor(editor): add bulk validation only on HARDSELECT_HOOK; skip SOFTSELECT_HOOK
Build / Build-and-ng-test (pull_request) Failing after 17m35s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 32m35s
2026-05-26 21:57:30 +02:00
sead 11ee49a57a feat(editor): validate autofilled cells; fix paste validation lag
Build / Build-and-ng-test (pull_request) Successful in 3m33s
Lighthouse Checks / lighthouse (pull_request) Failing after 27m56s
Build / Build-and-test-development (pull_request) Failing after 34m22s
2026-05-26 17:09:35 +02:00
sead 609731ff99 feat(editor): paste-validation overlay with cancel and confirm
Build / Build-and-ng-test (pull_request) Failing after 16m23s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 31m23s
2026-05-26 14:38:41 +02:00
sead d6cb32ed25 refactor(licensing): expand protocl text
Build / Build-and-ng-test (pull_request) Failing after 17m14s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 32m12s
2026-05-26 11:47:52 +02:00
sead 3668a7426f fix(licensing): add protocol info
Build / Build-and-ng-test (pull_request) Successful in 3m32s
Build / Build-and-test-development (pull_request) Failing after 20m50s
Lighthouse Checks / lighthouse (pull_request) Failing after 29m24s
Close #178
2026-05-26 11:40:40 +02:00
sead cc82dcaafe refactor(mocks): replace string webouts with JS objects
Build / Build-and-ng-test (pull_request) Failing after 17m42s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 28m24s
2026-05-26 11:06:50 +02:00
sead ea03bdecc5 fix(editor): await dynamic validation on paste; defer spinner 2026-05-26 10:48:13 +02:00
sead 51071b463b fix(editor): cancelEdit will reset cell's valid state 2026-05-25 16:55:18 +02:00
sead ac0bd10212 fix(handsontable): horizontal scrollbar in dropdown 2026-05-25 13:59:24 +02:00
sead 1b73e355b7 fix: migrate handsontables to v17 2026-05-25 13:29:27 +02:00
sead b661580c60 fix: validate pasted values 2026-05-22 22:10:58 +02:00
sead dc4e07a692 fix: viewLibs now fires only once, libPromise shared between the calls 2026-05-22 21:57:53 +02:00
semantic-release-bot f2313b31f1 chore(release): 7.8.2 [skip ci]
## [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](2382a559a5))
* enabling version restore for non admin users ([5d889d8](5d889d824c))
2026-05-20 13:20:34 +00:00
allan f8810ee7e9 Merge pull request 'fix: bumping ws package' (#234) from customerfeedback into main
Release / Build-production-and-ng-test (push) Successful in 3m17s
Release / Build-and-test-development (push) Successful in 8m27s
Release / release (push) Successful in 7m4s
Reviewed-on: #234
2026-05-20 13:05:56 +00:00
allan 8ab4af8397 Merge branch 'main' into customerfeedback
Build / Build-and-ng-test (pull_request) Successful in 3m29s
Build / Build-and-test-development (pull_request) Successful in 8m29s
Lighthouse Checks / lighthouse (pull_request) Successful in 17m59s
2026-05-20 12:45:51 +00:00
4gl 2382a559a5 fix: bumping ws package
Build / Build-and-ng-test (pull_request) Failing after 3m30s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 3m5s
2026-05-20 13:11:13 +01:00
Unknown 5d889d824c fix: enabling version restore for non admin users
Release / Build-production-and-ng-test (push) Failing after 1m11s
Release / Build-and-test-development (push) Has been skipped
Release / release (push) Has been skipped
2026-05-20 12:38:21 +01:00
semantic-release-bot bed21122ce chore(release): 7.8.1 [skip ci]
## [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](f1a26e132e))
2026-05-15 11:29:08 +00:00
allan ea8cf71101 Merge pull request 'fix(sasjs): enable runAsTask' (#233) from hotfix-sasjs-attributes into main
Release / Build-production-and-ng-test (push) Successful in 3m38s
Release / Build-and-test-development (push) Successful in 8m56s
Release / release (push) Successful in 8m0s
Reviewed-on: #233
2026-05-15 11:13:21 +00:00
sead f1a26e132e fix(sasjs): enable runAsTask
Build / Build-and-ng-test (pull_request) Successful in 3m57s
Build / Build-and-test-development (pull_request) Successful in 9m29s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m28s
2026-05-15 12:53:04 +02:00
semantic-release-bot 1db6984de3 chore(release): 7.8.0 [skip ci]
# [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](7d94cb2ae4))
* providing default values for RULE_ACTIVE on MPE_VALIDATIONS ([f031b4e](f031b4eb89))
* switch away from api usage for CASLIB metadata ([ce921a0](ce921a032a))
* use correct debug param for runAsTask ([bb80476](bb80476767))

### Features

* add runAsTask config attribute parser ([1635bc9](1635bc9c45))
* enabling *ALL* option by default in MPE_SECURITY (DSN col) ([93d4ab6](93d4ab65ac))
2026-05-15 09:07:49 +00:00
allan 636ff237dd Merge pull request 'Updates following customer session' (#231) from customerfeedback into main
Release / Build-production-and-ng-test (push) Successful in 3m39s
Release / Build-and-test-development (push) Successful in 8m50s
Release / release (push) Successful in 7m39s
Reviewed-on: #231
2026-05-15 08:52:14 +00:00
sead 02963ab6d5 chore: bump adapter
Build / Build-and-ng-test (pull_request) Successful in 3m49s
Build / Build-and-test-development (pull_request) Successful in 9m2s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m12s
2026-05-15 10:29:54 +02:00
allan d40f61292a Merge pull request 'feat: add runAsTask config attribute parser' (#232) from feat/execution-tasks-flag into customerfeedback
Lighthouse Checks / lighthouse (pull_request) Failing after 3m31s
Build / Build-and-ng-test (pull_request) Failing after 3m51s
Build / Build-and-test-development (pull_request) Has been skipped
Reviewed-on: #232
2026-05-15 08:08:39 +00:00
4gl 7d94cb2ae4 fix: enabling DSN=*ALL* in MPE_SECURITY
Build / Build-and-ng-test (pull_request) Successful in 3m48s
Build / Build-and-test-development (pull_request) Successful in 8m59s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m8s
2026-05-14 22:47:17 +01:00
sead bb80476767 fix: use correct debug param for runAsTask
Lighthouse Checks / lighthouse (pull_request) Failing after 3m28s
Build / Build-and-ng-test (pull_request) Failing after 3m51s
Build / Build-and-test-development (pull_request) Has been skipped
2026-05-14 11:21:01 +02:00
sead 1635bc9c45 feat: add runAsTask config attribute parser 2026-05-14 11:19:32 +02:00
4gl f031b4eb89 fix: providing default values for RULE_ACTIVE on MPE_VALIDATIONS
Lighthouse Checks / lighthouse (pull_request) Successful in 18m12s
Build / Build-and-ng-test (pull_request) Successful in 3m40s
Build / Build-and-test-development (pull_request) Successful in 8m58s
2026-05-13 19:02:09 +01:00
4gl 93d4ab65ac feat: enabling *ALL* option by default in MPE_SECURITY (DSN col)
Build / Build-and-ng-test (pull_request) Successful in 3m53s
Build / Build-and-test-development (pull_request) Successful in 9m12s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m15s
2026-05-13 18:47:19 +01:00
4gl ce921a032a fix: switch away from api usage for CASLIB metadata 2026-05-13 18:46:35 +01:00
semantic-release-bot 322f904b4b chore(release): 7.7.3 [skip ci]
## [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](65f0b979a4))
2026-05-12 18:43:04 +00:00
allan 982eeac58c Merge pull request 'fix: move cas session assign to settings.sas and abort when lib is unassigned' (#230) from viyaux into main
Release / Build-production-and-ng-test (push) Successful in 3m49s
Release / Build-and-test-development (push) Successful in 9m10s
Release / release (push) Successful in 8m2s
Reviewed-on: #230
2026-05-12 18:26:47 +00:00
allan 0ab9717556 Merge branch 'main' into viyaux
Build / Build-and-test-development (pull_request) Has been cancelled
Build / Build-and-ng-test (pull_request) Has been cancelled
Lighthouse Checks / lighthouse (pull_request) Successful in 18m27s
2026-05-12 18:26:35 +00:00
allan 24a85de8e1 Merge pull request 'chore(client): bump fast-uri' (#229) from audit-20260511 into main
Release / Build-production-and-ng-test (push) Successful in 3m40s
Release / Build-and-test-development (push) Successful in 8m50s
Release / release (push) Failing after 3m10s
Reviewed-on: #229
2026-05-11 17:38:14 +00:00
4gl 65f0b979a4 fix: move cas session assign to settings.sas and abort when lib is unassigned
Build / Build-and-ng-test (pull_request) Successful in 3m55s
Build / Build-and-test-development (pull_request) Successful in 9m13s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m48s
2026-05-11 15:08:20 +01:00
sead 947f34a0ad chore(client): bump fast-uri
Build / Build-and-ng-test (pull_request) Successful in 3m50s
Build / Build-and-test-development (pull_request) Successful in 9m6s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m19s
Resolve GHSA-q3j6-qgpj-74h6, GHSA-v39h-62p7-jpjc
2026-05-11 09:39:36 +02:00
semantic-release-bot 0f60fd7181 chore(release): 7.7.2 [skip ci]
## [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](9546fcd631))
* **client:** clear angular build cache on font strip to avoid stale dist ([503cb08](503cb08b2f))
* **client:** postinstall removal of Metropolis [@font-face](https://git.datacontroller.io/font-face) from @clr/ui ([e6397ce](e6397cecc1))
* **client:** serve text-security-disc font locally ([80ce80e](80ce80ece4))
* **editor:** preserve numeric type for SAS num cols with static SOFTSELECT/HARDSELECT ([05a3289](05a328976e))
2026-05-07 15:18:44 +00:00
allan 251062e42e Merge pull request 'Multiple frontend client issues' (#228) from 227-csp-issues-20260507 into main
Release / Build-production-and-ng-test (push) Successful in 3m38s
Release / Build-and-test-development (push) Successful in 8m50s
Release / release (push) Successful in 7m35s
Reviewed-on: #228
2026-05-07 15:03:08 +00:00
sead 05a328976e fix(editor): preserve numeric type for SAS num cols with static SOFTSELECT/HARDSELECT
Build / Build-and-ng-test (pull_request) Successful in 3m43s
Build / Build-and-test-development (pull_request) Successful in 9m21s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m25s
2026-05-07 15:02:06 +02:00
sead 503cb08b2f fix(client): clear angular build cache on font strip to avoid stale dist 2026-05-07 13:43:57 +02:00
sead f71be20476 build(client): drop duplicate asset emits for fonts and CSS-referenced svgs 2026-05-07 13:43:49 +02:00
sead e6397cecc1 fix(client): postinstall removal of Metropolis @font-face from @clr/ui 2026-05-07 13:43:37 +02:00
sead 80ce80ece4 fix(client): serve text-security-disc font locally 2026-05-07 13:43:21 +02:00
sead 9546fcd631 fix(client): bundle Metropolis font locally to satisfy CSP 2026-05-07 13:43:05 +02:00
semantic-release-bot b79aaf4327 chore(release): 7.7.1 [skip ci]
## [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](d26f7d2511))
* **sas:** bump cli ([d60029d](d60029deae))
2026-05-05 20:04:33 +00:00
allan 76f9198f73 Merge pull request 'fix(client): bump adapter' (#226) from fix/adapter-20260505 into main
Release / Build-production-and-ng-test (push) Successful in 3m28s
Release / Build-and-test-development (push) Successful in 8m44s
Release / release (push) Successful in 7m33s
Reviewed-on: #226
2026-05-05 19:49:17 +00:00
4gl d60029deae fix(sas): bump cli
Build / Build-and-ng-test (pull_request) Successful in 3m57s
Build / Build-and-test-development (pull_request) Successful in 9m9s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m10s
2026-05-05 18:48:15 +01:00
sead d26f7d2511 fix(client): bump adapter
Build / Build-and-ng-test (pull_request) Successful in 4m21s
Build / Build-and-test-development (pull_request) Successful in 10m34s
Lighthouse Checks / lighthouse (pull_request) Successful in 19m37s
2026-05-05 17:16:55 +02:00
semantic-release-bot 33efe09b50 chore(release): 7.7.0 [skip ci]
# [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](1707f3802a))
* remove data:image/svg+xml CSP violation, use class instead changing style directly ([d66eb5d](d66eb5dfc2))
* remove WORK, SASUSER and CASUSER as library options.  [#224](#224) ([ec66631](ec66631a33))

### Features

* auto-save CAS tables [#224](#224) ([40d04a5](40d04a53c4))
* autoload CAS tables. [#224](#224) ([d5ebb01](d5ebb01ce3))
2026-05-04 23:24:36 +00:00
4gl e0aef9bf00 chore: pin got lib to enable release flow
Release / Build-production-and-ng-test (push) Successful in 11m47s
Release / Build-and-test-development (push) Successful in 17m37s
Release / release (push) Successful in 8m18s
2026-05-04 23:51:19 +01:00
allan 02d1a2e0b1 Merge pull request 'fix: resolve CSP violation and update dependancies' (#223) from fix/audit-20260413 into main
Release / Build-production-and-ng-test (push) Successful in 8m48s
Release / Build-and-test-development (push) Successful in 17m50s
Release / release (push) Failing after 5m52s
Reviewed-on: #223
Reviewed-by: allan <allan@4gl.io>
2026-05-04 16:50:52 +00:00
sead 4e3154e929 chore(cypress): enable e2e video, folder guards
Build / Build-and-ng-test (pull_request) Successful in 3m58s
Build / Build-and-test-development (pull_request) Successful in 9m6s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m21s
2026-05-04 12:57:31 +02:00
sead 32c0713256 chore: add hyperformula license exception
Build / Build-and-ng-test (pull_request) Successful in 3m44s
Build / Build-and-test-development (pull_request) Failing after 8m45s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m29s
2026-05-04 11:05:00 +02:00
sead defe15bcec chore: bump client eslint and sas sasjs packages
Build / Build-and-ng-test (pull_request) Failing after 1m34s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 1m47s
2026-05-04 10:42:31 +02:00
4gl 6f8e471f16 chore: dep man
Build / Build-and-ng-test (pull_request) Failing after 40s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 55s
2026-05-01 12:53:07 +01:00
4gl dc35abfd85 chore: dependency bumps
Build / Build-and-ng-test (pull_request) Failing after 42s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 1m0s
2026-05-01 12:36:39 +01:00
4gl 04a8c5d52a chore: bumping node
Build / Build-and-ng-test (pull_request) Failing after 50s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 57s
2026-05-01 11:46:10 +01:00
4gl 2cb370053d chore: rebuilt package lock
Build / Build-and-ng-test (pull_request) Failing after 42s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Failing after 1m3s
2026-05-01 11:34:30 +01:00
4gl 1707f3802a fix: bump adapter to 4.16.6
Lighthouse Checks / lighthouse (pull_request) Failing after 1m7s
Build / Build-and-ng-test (pull_request) Failing after 1m44s
Build / Build-and-test-development (pull_request) Has been skipped
2026-05-01 10:59:59 +01:00
allan c87ba660ca Merge branch 'main' into fix/audit-20260413
Build / Build-and-ng-test (pull_request) Successful in 3m58s
Build / Build-and-test-development (pull_request) Successful in 9m56s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m30s
2026-04-30 16:44:46 +00:00
allan ef8a2dbc38 Merge pull request 'fix: remove WORK, SASUSER and CASUSER as library options, plus auto CAS table load' (#225) from issue224 into main
Release / Build-production-and-ng-test (push) Failing after 1m28s
Release / Build-and-test-development (push) Has been skipped
Release / release (push) Has been skipped
Reviewed-on: #225
2026-04-30 15:28:50 +00:00
4gl 40d04a53c4 feat: auto-save CAS tables #224
Build / Build-and-ng-test (pull_request) Successful in 4m2s
Build / Build-and-test-development (pull_request) Successful in 10m6s
Lighthouse Checks / lighthouse (pull_request) Successful in 19m5s
2026-04-30 16:04:31 +01:00
4gl d5ebb01ce3 feat: autoload CAS tables. #224
Build / Build-and-ng-test (pull_request) Successful in 4m6s
Build / Build-and-test-development (pull_request) Successful in 10m9s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m54s
2026-04-30 15:41:20 +01:00
allan ec66631a33 fix: remove WORK, SASUSER and CASUSER as library options. #224
Build / Build-and-ng-test (pull_request) Successful in 4m15s
Build / Build-and-test-development (pull_request) Successful in 10m28s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m47s
2026-04-17 14:21:24 +01:00
sead d66eb5dfc2 fix: remove data:image/svg+xml CSP violation, use class instead changing style directly
Build / Build-and-ng-test (pull_request) Failing after 1m16s
Build / Build-and-test-development (pull_request) Has been skipped
Lighthouse Checks / lighthouse (pull_request) Successful in 18m7s
2026-04-13 10:29:54 +02:00
sead 731b589ed8 chore: override ajv and regenrate lock file 2026-04-13 09:23:13 +02:00
sead fe92d5fc36 chore: bump angular to latest 19 2026-04-13 08:58:54 +02:00
sead a335b400f1 chore: bump @sasjs/adapter 2026-04-13 08:55:48 +02:00
semantic-release-bot f63e507ddf chore(release): 7.6.0 [skip ci]
# [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](52d58036a4))

### Features

* configurable email alerts.  Closes [#217](#217) ([2ccf0d1](2ccf0d1100))
2026-04-03 22:17:14 +00:00
allan 991cc0567d Merge pull request 'feat: configurable email alerts. Closes #217' (#222) from issue217 into main
Release / Build-production-and-ng-test (push) Successful in 3m42s
Release / Build-and-test-development (push) Successful in 10m10s
Release / release (push) Successful in 7m48s
Reviewed-on: #222
2026-04-03 21:09:11 +00:00
sead 52d58036a4 fix: add label and tooltip for libref download, sanitise input
Build / Build-and-ng-test (pull_request) Successful in 4m6s
Build / Build-and-test-development (pull_request) Successful in 10m13s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m37s
2026-04-03 19:55:42 +02:00
allan 26bff85792 chore: fix debug line
Build / Build-and-ng-test (pull_request) Successful in 4m47s
Build / Build-and-test-development (pull_request) Successful in 10m16s
Lighthouse Checks / lighthouse (pull_request) Successful in 19m41s
2026-04-03 18:35:48 +01:00
allan 2ccf0d1100 feat: configurable email alerts. Closes #217
Build / Build-and-ng-test (pull_request) Successful in 4m42s
Build / Build-and-test-development (pull_request) Has been cancelled
Lighthouse Checks / lighthouse (pull_request) Has been cancelled
2026-04-03 18:34:23 +01:00
semantic-release-bot 3be33186bc chore(release): 7.5.0 [skip ci]
# [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](66e98a96cb))
* allow CSV uploads with licence row limit ([5b260e4](5b260e4915)), closes [#213](#213)
* bumping cli and pinning versions in .npmrc ([80039f4](80039f4876))
* guard CSV upload with fileUpload licence flag ([ed40df6](ed40df6295))
* parse embed param from window.location.hash for hash router compatibility ([0269c24](0269c2421d))
* quote CSV char values.  Closes [#215](#215) ([d9980e8](d9980e866d))
* resolve outer promise in parseCsvFile for non-WLATIN1 path ([4ee15e1](4ee15e1b6e))
* use XLSX for CSV row truncation to handle new lines in values ([6d590c0](6d590c050d))

### Features

* add embed URL parameter to hide header and back button ([b0dc441](b0dc441d68)), closes [#214](#214)
* add target libref input to config download ([a89657b](a89657b0b8)), closes [#212](#212)
* export config service to allow dclib swapping.  Closes [#212](#212) ([326c26f](326c26fddf))
2026-04-03 11:06:36 +00:00
allan 1a7f950ae2 Merge pull request 'feat: enabling dclib switching when exporting config' (#220) from issue212 into main
Release / Build-production-and-ng-test (push) Successful in 3m39s
Release / Build-and-test-development (push) Successful in 9m55s
Release / release (push) Successful in 7m46s
Reviewed-on: #220
2026-04-03 10:49:43 +00:00
allan 8924dc8ab1 chore: merge buid.yaml
Build / Build-and-ng-test (pull_request) Successful in 3m58s
Build / Build-and-test-development (pull_request) Successful in 10m3s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m46s
2026-04-03 10:30:05 +00:00
sead 2c2901b537 chore: rever upload artifacts actions version
Build / Build-and-ng-test (pull_request) Successful in 4m3s
Build / Build-and-test-development (pull_request) Successful in 10m0s
Lighthouse Checks / lighthouse (pull_request) Successful in 18m24s
2026-04-03 10:32:28 +02:00
sead 2cae7ea638 chore: improve CI workflows
Build / Build-and-ng-test (pull_request) Successful in 4m3s
Build / Build-and-test-development (pull_request) Failing after 10m17s
Lighthouse Checks / lighthouse (pull_request) Failing after 18m31s
2026-04-03 09:36:39 +02:00
sead 66e98a96cb fix: add workflow audits, update deps
Build / Build-and-ng-test (pull_request) Successful in 4m2s
Build / Build-and-test-development (pull_request) Successful in 10m19s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m53s
2026-04-03 09:10:49 +02:00
allan 0b0db1c543 chore: run audit check in build.yaml as well as release.yaml
Build / Build-and-ng-test (pull_request) Failing after 1m31s
Build / Build-and-test-development (pull_request) Successful in 10m23s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m32s
2026-04-03 01:18:54 +00:00
allan 80039f4876 fix: bumping cli and pinning versions in .npmrc
Build / Build-and-ng-test (pull_request) Successful in 3m51s
Build / Build-and-test-development (pull_request) Successful in 10m9s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m11s
2026-04-03 02:02:05 +01:00
allan 326c26fddf feat: export config service to allow dclib swapping. Closes #212 2026-04-03 02:01:44 +01:00
allan e7b2ead0e2 Merge pull request 'fix: allow CSV uploads with licence row limit' (#219) from fix/213-csv-license-row-limit into main
Release / Build-production-and-ng-test (push) Failing after 1m25s
Release / Build-and-test-development (push) Has been skipped
Release / release (push) Has been skipped
Reviewed-on: #219
2026-04-02 19:08:18 +00:00
sead a89657b0b8 feat: add target libref input to config download
Build / Build-and-ng-test (pull_request) Successful in 4m5s
Build / Build-and-test-development (pull_request) Successful in 10m16s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m30s
Closes #212
2026-04-02 19:37:55 +02:00
sead 4ee15e1b6e fix: resolve outer promise in parseCsvFile for non-WLATIN1 path
Build / Build-and-ng-test (pull_request) Successful in 3m55s
Build / Build-and-test-development (pull_request) Successful in 10m21s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m49s
2026-04-02 18:48:27 +02:00
sead ed40df6295 fix: guard CSV upload with fileUpload licence flag
Build / Build-and-ng-test (pull_request) Successful in 4m3s
Build / Build-and-test-development (pull_request) Failing after 11m54s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m35s
2026-04-02 17:40:16 +02:00
sead 6d590c050d fix: use XLSX for CSV row truncation to handle new lines in values
Build / Build-and-ng-test (pull_request) Successful in 3m53s
Build / Build-and-test-development (pull_request) Successful in 10m25s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m29s
2026-04-02 17:03:16 +02:00
allan 47f9a54f97 Merge pull request 'feat: add embed URL parameter to hide header and back button' (#218) from feat/214-hide-titlebar-embed into fix/213-csv-license-row-limit
Build / Build-and-ng-test (pull_request) Successful in 4m0s
Build / Build-and-test-development (pull_request) Successful in 10m18s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m56s
Reviewed-on: #218
2026-04-02 14:37:06 +00:00
sead 17b0d72fbf test: add csv-limited spec to cypress workflow
Build / Build-and-ng-test (pull_request) Successful in 4m1s
Build / Build-and-test-development (pull_request) Successful in 10m23s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m1s
2026-04-02 16:13:35 +02:00
sead 0269c2421d fix: parse embed param from window.location.hash for hash router compatibility
Build / Build-and-ng-test (pull_request) Successful in 4m9s
Build / Build-and-test-development (pull_request) Successful in 10m9s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m8s
2026-04-02 14:57:16 +02:00
sead 5b260e4915 fix: allow CSV uploads with licence row limit
Build / Build-and-ng-test (pull_request) Successful in 3m56s
Build / Build-and-test-development (pull_request) Successful in 10m3s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m49s
Fixes #213
2026-04-02 14:34:58 +02:00
allan 5290410a17 Merge branch 'main' into feat/214-hide-titlebar-embed
Build / Build-and-ng-test (pull_request) Successful in 3m56s
Build / Build-and-test-development (pull_request) Successful in 10m11s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m31s
2026-04-02 11:13:49 +00:00
allan dc9041aaec Merge pull request 'fix: quote CSV char values. Closes #215' (#216) from issue215 into main
Release / Build-production-and-ng-test (push) Failing after 1m25s
Release / Build-and-test-development (push) Has been skipped
Release / release (push) Has been skipped
Reviewed-on: #216
2026-04-02 11:12:38 +00:00
sead b0dc441d68 feat: add embed URL parameter to hide header and back button
Build / Build-and-ng-test (pull_request) Successful in 4m3s
Build / Build-and-test-development (pull_request) Successful in 10m6s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m39s
Closes #214
2026-04-02 11:26:28 +02:00
allan b0fc3eb5af chore: update comment
Build / Build-and-ng-test (pull_request) Successful in 4m23s
Build / Build-and-test-development (pull_request) Successful in 10m7s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m13s
2026-03-31 17:09:17 +01:00
allan d9980e866d fix: quote CSV char values. Closes #215
Build / Build-and-ng-test (pull_request) Successful in 4m7s
Build / Build-and-test-development (pull_request) Has been cancelled
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Has been cancelled
2026-03-31 17:04:46 +01:00
semantic-release-bot 52ae3404ee chore(release): 7.4.1 [skip ci]
## [7.4.1](https://git.datacontroller.io/dc/dc/compare/v7.4.0...v7.4.1) (2026-03-12)

### Bug Fixes

* support for SASIOSNF engine (SNOW alias) plus meta assignment ([7694d1b](7694d1b0fb))
2026-03-12 00:52:17 +00:00
allan eecb4f4f53 Merge pull request 'fix: support for SASIOSNF engine (SNOW alias) plus meta assignment' (#209) from snowfixes into main
Release / Build-production-and-ng-test (push) Successful in 3m41s
Release / Build-and-test-development (push) Successful in 9m52s
Release / release (push) Successful in 7m57s
Reviewed-on: #209
2026-03-12 00:35:16 +00:00
allan 744345af81 chore: bump sasjs/cli
Build / Build-and-ng-test (pull_request) Successful in 3m51s
Build / Build-and-test-development (pull_request) Successful in 9m58s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m48s
2026-03-12 00:16:10 +00:00
_ 7694d1b0fb fix: support for SASIOSNF engine (SNOW alias) plus meta assignment
Build / Build-and-ng-test (pull_request) Successful in 3m46s
Build / Build-and-test-development (pull_request) Successful in 9m38s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m18s
2026-03-10 23:50:57 +00:00
127 changed files with 13664 additions and 16493 deletions
+49 -37
View File
@@ -2,39 +2,53 @@ name: Build
run-name: Running Lint Check and Licence checker on Pull Request
on: [pull_request]
env:
NODE_VERSION: '24.15.0'
jobs:
Build-and-ng-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24.5.0
node-version: ${{ env.NODE_VERSION }}
- name: Install Google Chrome
run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
apt-get update
apt-get install -y google-chrome-stable xvfb
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
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: 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
@@ -52,26 +66,27 @@ jobs:
Build-and-test-development:
runs-on: ubuntu-latest
needs: Build-production-and-ng-test
needs: Build-and-ng-test
env:
CHROME_BIN: /usr/bin/google-chrome
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24.5.0
node-version: ${{ env.NODE_VERSION }}
- name: Write .npmrc file
run: |
touch client/.npmrc
echo '${{ secrets.NPMRC}}' > client/.npmrc
- run: apt-get update
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- run: apt install -y ./google-chrome*.deb;
- run: export CHROME_BIN=/usr/bin/google-chrome
- run: apt-get update -y
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb
- run: apt -y install jq
- name: Install system dependencies
run: |
apt-get update
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome*.deb
apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb jq zip
- name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
@@ -86,17 +101,18 @@ jobs:
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
npm ci
# Install pm2 and prepare SASJS server
- run: npm i -g pm2
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- run: unzip linux.zip
- run: touch .env
- run: echo RUN_TIMES=js >> .env
- run: echo NODE_PATH=node >> .env
- run: echo CORS=enable >> .env
- run: echo WHITELIST=http://localhost:4200 >> .env
- run: cat .env
- run: pm2 start api-linux --wait-ready
- name: Setup and start SASjs server
run: |
npm i -g pm2
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
unzip linux.zip
touch .env
echo RUN_TIMES=js >> .env
echo NODE_PATH=node >> .env
echo CORS=enable >> .env
echo WHITELIST=http://localhost:4200 >> .env
cat .env
pm2 start api-linux --wait-ready
- name: Deploy mocked services
run: |
@@ -106,11 +122,6 @@ jobs:
sasjs cbd -t server-ci
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
- name: Install ZIP
run: |
apt-get update
apt-get install zip
- name: Prepare and run frontend and cypress
run: |
cd ./client
@@ -126,11 +137,12 @@ jobs:
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
cat ./cypress.config.ts
# Start frontend and run cypress
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
- name: Zip Cypress videos
if: always()
run: |
mkdir -p ./client/cypress/videos
zip -r cypress-videos ./client/cypress/videos
- name: Add cypress videos artifacts
+18 -28
View File
@@ -2,38 +2,31 @@ name: Lighthouse Checks
run-name: Running Lighthouse Performance and Accessibility Checks on Pull Request
on: [pull_request]
env:
NODE_VERSION: '24.15.0'
jobs:
lighthouse:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24.5.0]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
node-version: ${{ env.NODE_VERSION }}
- name: Install Google Chrome
run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
apt-get update
apt-get install -y google-chrome-stable xvfb
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome*.deb
- name: Install pm2 for process management
run: npm i -g pm2
- name: Install global packages
run: npm i -g pm2 @sasjs/cli wait-on
- name: Install @sasjs/cli
run: npm i -g @sasjs/cli
- name: Install wait-on globally
run: npm install -g wait-on
- name: Create .env file for sasjs/server
- name: Setup and start SASjs server
run: |
touch .env
echo RUN_TIMES=js >> .env
@@ -41,15 +34,9 @@ jobs:
echo CORS=enable >> .env
echo WHITELIST=http://localhost:4200 >> .env
cat .env
- name: Download sasjs/server package from github using curl
run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- name: Unzip downloaded package
run: unzip linux.zip
- name: Run sasjs server
run: pm2 start api-linux --wait-ready
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
unzip linux.zip
pm2 start api-linux --wait-ready
- name: Write .npmrc file
run: echo "$NPMRC" > client/.npmrc
@@ -61,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
+38 -42
View File
@@ -5,15 +5,20 @@ on:
branches:
- main
env:
NODE_VERSION: '24.5.0'
jobs:
Build-production-and-ng-test:
runs-on: ubuntu-latest
env:
CHROME_BIN: /usr/bin/google-chrome
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24.5.0
node-version: ${{ env.NODE_VERSION }}
- name: Write .npmrc file
run: |
@@ -24,8 +29,7 @@ jobs:
run: |
apt-get update
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome*.deb;
export CHROME_BIN=/usr/bin/google-chrome
apt install -y ./google-chrome*.deb
- name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
@@ -43,9 +47,9 @@ jobs:
- name: Check audit
# Audit should fail and stop the CI if critical vulnerability found
run: |
npm audit --audit-level=critical --omit=dev
npm audit --omit=dev
cd ./sas
npm audit --audit-level=critical --omit=dev
npm audit --omit=dev
cd ../client
npm audit --audit-level=critical --omit=dev
@@ -63,25 +67,26 @@ jobs:
Build-and-test-development:
runs-on: ubuntu-latest
needs: Build-production-and-ng-test
env:
CHROME_BIN: /usr/bin/google-chrome
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24.5.0
node-version: ${{ env.NODE_VERSION }}
- name: Write .npmrc file
run: |
touch client/.npmrc
echo '${{ secrets.NPMRC}}' > client/.npmrc
- run: apt-get update
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- run: apt install -y ./google-chrome*.deb;
- run: export CHROME_BIN=/usr/bin/google-chrome
- run: apt-get update -y
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb
- run: apt -y install jq
- name: Install system dependencies
run: |
apt-get update
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome*.deb
apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb jq zip
- name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
@@ -96,17 +101,18 @@ jobs:
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
npm ci
# Install pm2 and prepare SASJS server
- run: npm i -g pm2
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- run: unzip linux.zip
- run: touch .env
- run: echo RUN_TIMES=js >> .env
- run: echo NODE_PATH=node >> .env
- run: echo CORS=enable >> .env
- run: echo WHITELIST=http://localhost:4200 >> .env
- run: cat .env
- run: pm2 start api-linux --wait-ready
- name: Setup and start SASjs server
run: |
npm i -g pm2
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
unzip linux.zip
touch .env
echo RUN_TIMES=js >> .env
echo NODE_PATH=node >> .env
echo CORS=enable >> .env
echo WHITELIST=http://localhost:4200 >> .env
cat .env
pm2 start api-linux --wait-ready
- name: Deploy mocked services
run: |
@@ -116,11 +122,6 @@ jobs:
sasjs cbd -t server-ci
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
- name: Install ZIP
run: |
apt-get update
apt-get install zip
- name: Prepare and run frontend and cypress
run: |
cd ./client
@@ -136,11 +137,12 @@ jobs:
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
cat ./cypress.config.ts
# Start frontend and run cypress
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
- name: Zip Cypress videos
if: always()
run: |
mkdir -p ./client/cypress/videos
zip -r cypress-videos ./client/cypress/videos
- name: Add cypress videos artifacts
@@ -155,10 +157,10 @@ jobs:
needs: [Build-production-and-ng-test, Build-and-test-development]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24.5.0
node-version: ${{ env.NODE_VERSION }}
- name: Write .npmrc file
run: |
@@ -168,17 +170,11 @@ jobs:
env:
NPMRC: ${{ secrets.NPMRC}}
- name: Install packages
- name: Install system packages
run: |
apt-get update
apt-get install zip -y
# sasjs cli is used to compile & build the SAS services
apt-get install -y zip jq doxygen
npm i -g @sasjs/cli
# jq is used to parse the release JSON
apt-get install jq -y
# doxygen is used for the SASJS docs
apt-get update
apt-get install doxygen -y
- name: Frontend Preliminary Build
description: We want to prevent creating empty release if frontend fails
+3
View File
@@ -1 +1,4 @@
legacy-peer-deps=true
ignore-scripts=true
save-exact=true
fund=false
+112
View File
@@ -1,3 +1,115 @@
## [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)
### Bug Fixes
* support for SASIOSNF engine (SNOW alias) plus meta assignment ([7694d1b](https://git.datacontroller.io/dc/dc/commit/7694d1b0fb2bd0407c8598147fbae87a00d889a8))
# [7.4.0](https://git.datacontroller.io/dc/dc/compare/v7.3.0...v7.4.0) (2026-02-20)
+2 -1
View File
@@ -62,7 +62,8 @@
{
"glob": "**/*",
"input": "src/images",
"output": "images"
"output": "images",
"ignore": ["spinner.svg", "caret.svg"]
}
],
"styles": ["src/styles.scss"],
+18 -17
View File
@@ -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
},
},
});
}
}
})
+95
View File
@@ -0,0 +1,95 @@
const username = Cypress.env('username')
const password = Cypress.env('password')
const hostUrl = Cypress.env('hosturl')
const appLocation = Cypress.env('appLocation')
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
const serverType = Cypress.env('serverType')
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
const fixturePath = 'csvs/'
context('csv file upload restriction (free tier): ', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
cy.get('body').then(($body) => {
const usernameInput = $body.find('input.username')[0]
if (usernameInput && !Cypress.dom.isHidden(usernameInput)) {
cy.get('input.username').type(username)
cy.get('input.password').type(password)
cy.get('.login-group button').click()
}
})
cy.get('.app-loading', { timeout: longerCommandTimeout }).should(
'not.exist'
)
// Skip licensing page if presented - continue with free tier
cy.url().then((url) => {
if (url.includes('licensing')) {
cy.get('button').contains('Continue with free tier').click()
}
})
visitPage('home')
})
it('1 | File upload is restricted on free tier', () => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
// Click upload button - should show feature locked modal
cy.get('.buttonBar button:last-child').should('exist').click()
cy.get('.modal-title').should('contain', 'Locked Feature (File Upload)')
})
})
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
cy.get('.app-loading', { timeout: longerCommandTimeout })
.should('not.exist')
.then(() => {
cy.get('.nav-tree clr-tree > clr-tree-node', {
timeout: longerCommandTimeout
}).then((treeNodes: any) => {
let targetLib
for (let node of treeNodes) {
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
targetLib = node
break
}
}
cy.get(targetLib).within(() => {
cy.get('.clr-tree-node-content-container > button').click()
cy.get('.clr-treenode-link').then((innerNodes: any) => {
for (let innerNode of innerNodes) {
if (innerNode.innerText.toLowerCase().includes(tablename)) {
innerNode.click()
break
}
}
})
})
})
})
}
const attachFile = (filename: string, callback?: any) => {
cy.get('.buttonBar button:last-child')
.should('exist')
.click()
.then(() => {
cy.get('input[type="file"]#file-upload')
.attachFile(`/${fixturePath}/${filename}`)
.then(() => {
if (callback) callback()
})
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
+5 -1
View File
@@ -4,7 +4,11 @@ PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_
2,even more dummy data,Option 3,42,12FEB1960,01JAN1960:00:00:42,0:02:22,3,44
3,"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:",Option 2,1613.001,27FEB1961,01JAN1960:00:07:03,0:00:44,3,44
4,if you can fill the unforgiving minute,Option 1,1613.0011235,02AUG1971,29MAY1973:06:12:03,0:06:52,3,44
1010,10 bottles of beer on the wall,Option 1,0.9153696885,04MAR1962,01JAN1960:12:47:55,0:01:40,92,76
1010,"10 bottles of beer
on the wall",Option 1,0.9153696885,04MAR1962,01JAN1960:12:47:55,0:01:40,92,76
1011,11 bottles of beer on the wall,Option 1,0.3531217558,29MAR1960,01JAN1960:03:33:24,0:01:03,80,29
1012,12 bottles of beer on the wall,Option 1,0.6743748717,02AUG1962,01JAN1960:07:25:59,0:00:10,16,98
1013,13 bottles of beer on the wall,Option 1,0.1305445992,11SEP1960,01JAN1960:13:51:32,0:00:35,73,15
1 PRIMARY_KEY_FIELD SOME_CHAR SOME_DROPDOWN SOME_NUM SOME_DATE SOME_DATETIME SOME_TIME SOME_SHORTNUM SOME_BESTNUM
4 2 even more dummy data Option 3 42 12FEB1960 01JAN1960:00:00:42 0:02:22 3 44
5 3 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: Option 2 1613.001 27FEB1961 01JAN1960:00:07:03 0:00:44 3 44
6 4 if you can fill the unforgiving minute Option 1 1613.0011235 02AUG1971 29MAY1973:06:12:03 0:06:52 3 44
7 1010 10 bottles of beer on the wall 10 bottles of beer on the wall Option 1 0.9153696885 04MAR1962 01JAN1960:12:47:55 0:01:40 92 76
8 1011 11 bottles of beer on the wall Option 1 0.3531217558 29MAR1960 01JAN1960:03:33:24 0:01:03 80 29
9 1012 12 bottles of beer on the wall Option 1 0.6743748717 02AUG1962 01JAN1960:07:25:59 0:00:10 16 98
10 1013 13 bottles of beer on the wall Option 1 0.1305445992 11SEP1960 01JAN1960:13:51:32 0:00:35 73 15
11 1014 14 bottles of beer on the wall Option 1 0.7409067949 26JUL1960 01JAN1960:05:18:10 0:00:41 30 89
12 1011 1015 11 bottles of beer on the wall 15 bottles of beer on the wall Option 1 0.3531217558 0.0869016028 29MAR1960 28FEB1961 01JAN1960:03:33:24 01JAN1960:13:23:45 0:01:03 0:00:44 80 29 3
13 1012 1016 12 bottles of beer on the wall 16 bottles of beer on the wall Option 1 0.6743748717 0.0462121419 02AUG1962 09AUG1962 01JAN1960:07:25:59 01JAN1960:07:42:38 0:00:10 0:01:17 16 62 98 2
14 1013 1017 13 bottles of beer on the wall 17 bottles of beer on the wall Option 1 0.1305445992 0.7501918947 11SEP1960 14MAY1962 01JAN1960:13:51:32 01JAN1960:04:40:20 0:00:35 0:00:15 73 53 15 65
+1 -1
View File
@@ -10,7 +10,7 @@ const check = (cwd) => {
onlyAllow:
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
excludePackages:
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;handsontable@^16.0.1;handsontable@16.2.0;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;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) {
+5357 -6503
View File
File diff suppressed because it is too large Load Diff
+25 -22
View File
@@ -23,7 +23,7 @@
"watch": "ng test watch=true",
"pree2e": "webdriver-manager update",
"e2e": "protractor protractor.config.js",
"postinstall": "node ./src/version.ts && npm run add-githook",
"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,12 +62,12 @@
"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",
"jquery-datetimepicker": "^2.5.21",
"jsrsasign": "^11.1.0",
"jsrsasign": "11.1.1",
"marked": "^5.0.0",
"moment": "^2.30.1",
"ngx-clipboard": "^16.0.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"
}
}
+69
View File
@@ -0,0 +1,69 @@
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'fs'
import { resolve, join } from 'path'
import { createRequire } from 'module'
/**
* Generate static SVG assets + an SCSS partial that re-applies HOT v17 classic
* theme icons via real URLs (not data: URIs).
*
* Why: deployed app runs under CSP `img-src 'self'`. HOT v17's classic theme
* embeds icons as `data:image/svg+xml,...` in `-webkit-mask-image` rules, which
* the CSP blocks. We switch to `ht-theme-classic-no-icons.min.css` and re-add
* the icon rules pointing at same-origin SVG files emitted from this script.
*
* Inputs (HOT's own modules, so semantic names + selector list track upstream):
* handsontable/themes/theme/classic → { classicTheme: { icons } }
* handsontable/themes/static/variables/helpers/iconsMap → iconsMap(icons, themePrefix)
*
* Outputs:
* client/src/assets/hot-icons/<kebab-name>.svg
* client/src/_hot-icons.scss
*
* Idempotent: clears the output dir and rewrites both outputs each run.
* Skips silently if handsontable isn't installed yet (pre-install runs).
*/
const require = createRequire(import.meta.url)
const ASSETS_DIR = resolve('src/assets/hot-icons')
const SCSS_OUT = resolve('src/_hot-icons.scss')
const ASSET_URL_PREFIX = './assets/hot-icons/'
const themePath = resolve('node_modules/handsontable/themes/theme/classic.js')
const mapPath = resolve('node_modules/handsontable/themes/static/variables/helpers/iconsMap.js')
if (!existsSync(themePath) || !existsSync(mapPath)) {
console.log('skip: handsontable theme modules not found (likely pre-install run)')
process.exit(0)
}
const { classicTheme } = require(themePath)
const { iconsMap } = require(mapPath)
const icons = classicTheme.icons
const cssTemplate = iconsMap(icons, 'ht-theme-classic')
const kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
rmSync(ASSETS_DIR, { recursive: true, force: true })
mkdirSync(ASSETS_DIR, { recursive: true })
const writeMap = {}
for (const [name, dataUri] of Object.entries(icons)) {
if (typeof dataUri !== 'string' || !dataUri.startsWith('data:image/svg+xml')) continue
const decoded = decodeURIComponent(dataUri.replace(/^data:image\/svg\+xml(;charset=utf-8)?,/, ''))
const fname = kebab(name) + '.svg'
writeFileSync(join(ASSETS_DIR, fname), decoded)
writeMap[dataUri] = ASSET_URL_PREFIX + fname
}
let scss = cssTemplate
for (const [uri, url] of Object.entries(writeMap)) {
scss = scss.split(`url("${uri}")`).join(`url("${url}")`)
}
const header = '/* Auto-generated by scripts/gen-hot-icons.mjs — do not edit by hand.\n' +
' Regenerated on postinstall; rerun manually via `node scripts/gen-hot-icons.mjs`. */\n\n'
writeFileSync(SCSS_OUT, header + scss + '\n')
console.log(`hot-icons: wrote ${Object.keys(writeMap).length} SVGs + ${SCSS_OUT}`)
+59
View File
@@ -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)`)
}
+238
View File
@@ -0,0 +1,238 @@
/* Auto-generated by scripts/gen-hot-icons.mjs — do not edit by hand.
Regenerated on postinstall; rerun manually via `node scripts/gen-hot-icons.mjs`. */
[class*=ht-theme-classic] .htDropdownMenu table tbody tr td.htSubmenu .htItemWrapper::after,
[class*=ht-theme-classic] .htContextMenu table tbody tr td.htSubmenu .htItemWrapper::after,
[class*=ht-theme-classic] .htFiltersConditionsMenu table tbody tr td.htSubmenu .htItemWrapper::after,
[class*=ht-theme-classic] .pika-single .pika-next {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-right.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .pika-single .pika-prev {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-left.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-page-size-section__select-wrapper::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-down.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .changeType::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/select-arrow.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .htUISelectCaption::after,
.htAutocompleteArrow::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/select-arrow.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .columnSorting.sortAction.ascending::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-narrow-up.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .columnSorting.sortAction.descending::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-narrow-down.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-page-navigation-section .ht-page-first::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-left-with-bar.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] [dir="rtl"] .ht-page-navigation-section .ht-page-first::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-right-with-bar.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-page-navigation-section .ht-page-prev::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-left.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] [dir="rtl"] .ht-page-navigation-section .ht-page-prev::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-right.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-page-navigation-section .ht-page-next::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-right.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] [dir="rtl"] .ht-page-navigation-section .ht-page-next::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-left.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-page-navigation-section .ht-page-last::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-right-with-bar.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] [dir="rtl"] .ht-page-navigation-section .ht-page-last::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/arrow-left-with-bar.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .htDropdownMenu table tbody tr td .htItemWrapper span.selected::after,
[class*=ht-theme-classic] .htContextMenu table tbody tr td .htItemWrapper span.selected::after,
[class*=ht-theme-classic] .htFiltersConditionsMenu table tbody tr td .htItemWrapper span.selected::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/check.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .htCheckboxRendererInput {
appearance: none;
}
[class*=ht-theme-classic] .htCheckboxRendererInput::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/checkbox.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] th.beforeHiddenColumn::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/caret-hidden-left.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] th.afterHiddenColumn::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/caret-hidden-right.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] th.beforeHiddenRow::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/caret-hidden-up.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] th.afterHiddenRow::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/caret-hidden-down.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .collapsibleIndicator::before,
[class*=ht-theme-classic] .ht_nestingButton::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/collapse-off.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .collapsibleIndicator.collapsed::before,
[class*=ht-theme-classic] .ht_nestingButton.ht_nestingExpand::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/collapse-on.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .htUIRadio > input[type="radio"]::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/radio.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-multi-select-chip-remove::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/chip-close.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-notification__close::before {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/chip-close.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-multi-select-editor-search-icon {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/search.svg");
background-color: currentColor;
}
[class*=ht-theme-classic] .ht-multi-select-editor-item-selected input::after {
width: var(--ht-icon-size);
height: var(--ht-icon-size);
-webkit-mask-size: contain;
-webkit-mask-image: url("./assets/hot-icons/checkbox.svg");
background-color: currentColor;
}
+2
View File
@@ -55,6 +55,7 @@ export interface HandsontableStaticConfig {
* Cached viyaApi collections, search and selected endpoint
*/
export const globals: {
embed: boolean
rootParam: string
dcLib: string
xlmaps: XLMapListItem[]
@@ -69,6 +70,7 @@ export const globals: {
handsontable: HandsontableStaticConfig
[key: string]: any
} = {
embed: false,
rootParam: <string>'',
dcLib: '',
xlmaps: [],
+4 -3
View File
@@ -107,7 +107,7 @@
</div>
</ng-container>
<header class="app-header">
<header class="app-header" *ngIf="!embed">
<!-- <button
*ngIf="
isMainRoute('view') ||
@@ -213,9 +213,10 @@
</header>
<nav
*ngIf="
router.url.includes('submitted') ||
!embed &&
(router.url.includes('submitted') ||
router.url.includes('approve') ||
router.url.includes('history')
router.url.includes('history'))
"
class="subnav"
>
+12
View File
@@ -70,6 +70,7 @@ export class AppComponent {
public syssite = this.appService.syssite
public licenceState = this.licenceService.licenceState
public embed = globals.embed
constructor(
private appService: AppService,
@@ -143,6 +144,16 @@ export class AppComponent {
}
})
const hashQuery = window.location.hash.split('?')[1]
if (hashQuery) {
const embedParam = new URLSearchParams(hashQuery).get('embed')
if (embedParam !== null) {
const isEmbed = embedParam !== 'false'
globals.embed = isEmbed
this.embed = isEmbed
}
}
this.subscribeToShowAbortModal()
this.subscribeToRequestsModal()
this.subscribeToStartupData()
@@ -198,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)
}
/**
+15 -1
View File
@@ -165,7 +165,7 @@
class="card-header clr-row buttonBar headerBar clr-flex-md-row clr-justify-content-center clr-justify-content-lg-end"
>
<div
*ngIf="tableTrue"
*ngIf="tableTrue && !embed"
class="clr-col-12 clr-col-md-3 clr-col-lg-4 backBtn"
>
<span
@@ -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>
+374 -51
View File
@@ -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'
}
}
@@ -264,6 +268,9 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
public badEdit = false
public badEditCause: string | undefined
public badEditTitle: string | undefined
get embed() {
return globals.embed
}
public tableTrue: boolean | undefined
public saveLoading = false
public approvers: string[] = []
@@ -353,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
@@ -959,6 +988,8 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
}
cancelEdit() {
this.cancelBulkValidation({ revert: false })
this.toggleHotPlugin('contextMenu', false)
this.cellValidationSource = []
@@ -988,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)
@@ -997,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) {
@@ -1666,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
@@ -1965,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
@@ -1997,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
@@ -2072,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: [
{
@@ -2111,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
@@ -2137,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.
@@ -2195,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(
@@ -2219,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(
@@ -2233,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() {
@@ -2934,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
@@ -2952,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')
@@ -3016,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
+1 -1
View File
@@ -30,7 +30,7 @@ export const freeTierConfig: LicenceState = {
lineage_daily_limit: 3,
tables_in_library_limit: 35,
viewbox: true,
fileUpload: true,
fileUpload: false,
editRecord: true,
addRecord: true
}
@@ -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',
+13 -3
View File
@@ -155,13 +155,23 @@ export class SasStoreService {
.adapterResponse
}
private libsPromise: Promise<any> | null = null
/**
*
* @returns All libraries
*/
public async viewLibs() {
return (await this.sasService.request('public/viewlibs', null))
.adapterResponse
public viewLibs() {
if (!this.libsPromise) {
this.libsPromise = this.sasService
.request('public/viewlibs', null)
.then((res: any) => res.adapterResponse)
.catch((err: any) => {
this.libsPromise = null
throw err
})
}
return this.libsPromise
}
public async refreshLibInfo(libref: string) {
+17
View File
@@ -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
+8 -2
View File
@@ -15,6 +15,8 @@ import { DirectivesModule } from '../directives/directives.module'
import { DatasetInfoComponent } from './dataset-info/dataset-info.component'
import { ContactLinkComponent } from './contact-link/contact-link.component'
import { ExcelPasswordModalComponent } from './excel-password-modal/excel-password-modal.component'
import { ConfirmModalComponent } from './confirm-modal/confirm-modal.component'
import { BulkValidationModalComponent } from './bulk-validation-modal/bulk-validation-modal.component'
@NgModule({
imports: [
@@ -32,7 +34,9 @@ import { ExcelPasswordModalComponent } from './excel-password-modal/excel-passwo
TermsComponent,
DatasetInfoComponent,
ContactLinkComponent,
ExcelPasswordModalComponent
ExcelPasswordModalComponent,
ConfirmModalComponent,
BulkValidationModalComponent
],
exports: [
LoadingIndicatorComponent,
@@ -42,7 +46,9 @@ import { ExcelPasswordModalComponent } from './excel-password-modal/excel-passwo
TermsComponent,
DatasetInfoComponent,
ContactLinkComponent,
ExcelPasswordModalComponent
ExcelPasswordModalComponent,
ConfirmModalComponent,
BulkValidationModalComponent
],
providers: [UserService, AlertsService]
})
@@ -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'])
@@ -375,38 +375,30 @@ export class SpreadsheetUtil {
fileType: string
): Promise<ParseResult> {
return new Promise((resolve, reject) => {
if (this.licenceState.value.submit_rows_limit !== Infinity) {
if (!this.licenceState.value.fileUpload) {
uploader.queue.pop()
return reject(
'Excel files only. To unlock CSV uploads, please contact support@datacontroller.io'
'File uploads are not enabled for this licence. Please contact support@datacontroller.io'
)
}
if (parseParams.encoding === 'WLATIN1') {
let reader = new FileReader()
const self = this
// Closure to capture the file information.
reader.onload = (theFile: any) => {
let encoded = iconv.decode(
Buffer.from(theFile.target.result),
'CP-1252'
)
let blob = new Blob([encoded], { type: fileType })
let encodedFile: File = blobToFile(blob, parseParams.file.name)
if (parseParams.encoding !== 'WLATIN1') return resolve({ uploader })
const reader = new FileReader()
reader.onload = (theFile) => {
if (!theFile.target?.result) return resolve({ uploader })
const text = theFile.target.result as string
const encoded = iconv.encode(text, 'CP-1252')
const blob = new Blob([encoded], { type: fileType })
const encodedFile: File = blobToFile(blob, parseParams.file.name)
uploader.queue.pop()
uploader.addToQueue([encodedFile])
return resolve({
uploader
})
return resolve({ uploader })
}
reader.readAsArrayBuffer(parseParams.file)
} else {
return resolve({
uploader
})
}
reader.readAsText(parseParams.file)
})
}
+6
View File
@@ -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
+2 -1
View File
@@ -75,7 +75,8 @@ export class StageComponent implements OnInit, AfterViewInit {
afterInit: this.hotTable.afterInit,
stretchH: 'all',
cells: this.hotTable.cells,
className: 'htDark'
className: 'htDark',
theme: 'ht-theme-classic'
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ const routes: Routes = [{ path: ':tableId', component: StageComponent }]
CommonModule,
ClarityModule,
RouterModule.forChild(routes),
HotTableModule.forRoot()
HotTableModule
]
})
export class StageModule {}
+30 -1
View File
@@ -236,7 +236,36 @@
<div class="admin-action">
Download Configuration
<button (click)="downloadConfiguration()" class="btn btn-info btn-sm">
<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="e.g. MYLIB"
/>
</div>
<button
(click)="downloadConfiguration()"
[disabled]="targetLibref !== dcLib && !isValidLibref(targetLibref)"
class="btn btn-info btn-sm"
>
DOWNLOAD
</button>
</div>
@@ -0,0 +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;
text-transform: uppercase;
}
+11
View File
@@ -10,6 +10,7 @@ import { EnvironmentInfo } from './models/environment-info.model'
import { AppSettingsService } from '../services/app-settings.service'
import { AppSettings } from '../models/AppSettings'
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
import { globals } from '../_globals'
@Component({
selector: 'app-system',
@@ -39,6 +40,8 @@ export class SystemComponent implements OnInit {
responseModal: boolean = false
Infinity = Infinity
dcLib: string = globals.dcLib
targetLibref: string = globals.dcLib
licenceState = this.licenceService.licenceState
settings: AppSettings
@@ -71,13 +74,21 @@ export class SystemComponent implements OnInit {
this.appSettingsService.setAppSettings(this.settings)
}
isValidLibref(value: string): boolean {
return /^[A-Za-z_]\w{0,7}$/.test(value.trim())
}
downloadConfiguration() {
let sasjsConfig = this.sasService.getSasjsConfig()
let storage = sasjsConfig.serverUrl
let metaData = sasjsConfig.appLoc
let path = this.sasService.getExecutionPath()
let lib = this.targetLibref.toUpperCase().trim()
let downUrl =
storage + path + '/?_program=' + metaData + '/services/admin/exportconfig'
if (lib && lib !== this.dcLib && this.isValidLibref(lib)) {
downUrl += '&dclib=' + encodeURIComponent(lib)
}
window.open(downUrl)
}
+34 -10
View File
@@ -33,6 +33,7 @@ import {
Version
} from '../models/sas/editors-getdata.model'
import { mergeColsRules } from '../shared/dc-validator/utils/mergeColsRules'
import { makeNumberFormatRenderer } from '../editor/utils/renderers.utils'
import { PublicViewtablesServiceResponse } from '../models/sas/public-viewtables.model'
import { PublicViewlibsServiceResponse } from '../models/sas/public-viewlibs.model'
import { PublicRefreshlibinfoServiceResponse } from '../models/sas/public-refreshlibinfo.model'
@@ -122,6 +123,7 @@ export class ViewerComponent
stretchH: 'all',
modifyColWidth: this.maxWidthCheker,
cells: this.hotTable.cells,
hiddenColumns: { columns: this.hiddenViewColumns, indicators: false },
maxRows: this.hotTable.maxRows,
manualColumnResize: true,
afterGetColHeader: this.hotTable.afterGetColHeader,
@@ -129,12 +131,15 @@ export class ViewerComponent
rowHeaderWidth: this.hotTable.rowHeaderWidth,
rowHeights: this.hotTable.rowHeights,
licenseKey: this.hotTable.licenseKey,
className: 'htDark'
className: 'htDark',
theme: 'ht-theme-classic'
}
}
public numberOfRows: number | null = null
public headerPks: string[] = []
public $dataFormats: $DataFormats | null = null
// Physical column indexes to hide in the viewer (from HIDDEN DQ rules)
public hiddenViewColumns: number[] = []
public datasetInfo: boolean = false
public dsmeta: DSMeta[] = []
public versions: Version[] = []
@@ -769,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
+2 -1
View File
@@ -162,7 +162,8 @@ export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
rowHeaderWidth: 15,
rowHeights: 20,
licenseKey: this.hotTableLicenseKey,
className: 'htDark'
className: 'htDark',
theme: 'ht-theme-classic'
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'><g opacity='0.6'><path d='M11.5859 6L8.29304 9.29289L5.00015 6' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></g></svg>

After

Width:  |  Height:  |  Size: 239 B

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

After

Width:  |  Height:  |  Size: 262 B

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

After

Width:  |  Height:  |  Size: 231 B

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

After

Width:  |  Height:  |  Size: 271 B

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

After

Width:  |  Height:  |  Size: 279 B

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

After

Width:  |  Height:  |  Size: 252 B

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

After

Width:  |  Height:  |  Size: 231 B

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

After

Width:  |  Height:  |  Size: 727 B

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

After

Width:  |  Height:  |  Size: 800 B

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

After

Width:  |  Height:  |  Size: 815 B

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

After

Width:  |  Height:  |  Size: 826 B

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

After

Width:  |  Height:  |  Size: 231 B

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

After

Width:  |  Height:  |  Size: 197 B

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

After

Width:  |  Height:  |  Size: 203 B

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

After

Width:  |  Height:  |  Size: 201 B

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

After

Width:  |  Height:  |  Size: 639 B

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

After

Width:  |  Height:  |  Size: 799 B

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

After

Width:  |  Height:  |  Size: 151 B

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

After

Width:  |  Height:  |  Size: 948 B

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

After

Width:  |  Height:  |  Size: 799 B

+2 -1
View File
@@ -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"
+87 -15
View File
@@ -1,5 +1,8 @@
/* You can add global styles to this file, and also import other style files */
@import '~handsontable/dist/handsontable.full.css';
@import '~handsontable/styles/handsontable.min.css';
@import '~handsontable/styles/handsontable.min.css';
@import '~handsontable/styles/ht-theme-classic-no-icons.min.css';
@import './_hot-icons.scss';
@import '~@clr/icons/clr-icons.min.css';
@@ -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 {
+1892 -2361
View File
File diff suppressed because it is too large Load Diff
+8 -5
View File
@@ -1,17 +1,20 @@
{
"name": "dcfrontend",
"version": "7.4.0",
"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",
+2
View File
@@ -0,0 +1,2 @@
ignore-scripts=true
save-exact=true
+1 -1
View File
@@ -1,7 +1,7 @@
{
"fromjs": [
{
"ADMIN": "DCDEFAULT",
"ADMIN": "AllUsers",
"DCPATH": "/tmp/dcdata"
}
]
+330 -517
View File
@@ -1,521 +1,334 @@
_webout=`{"SYSDATE" : "26SEP22"
,"SYSTIME" : "08:30"
, "approvers":
[
{
"PERSONNAME": "sasdemo",
"EMAIL": "sasdemo",
"USERID": "sasdemo"
function makeRows(n) {
const dropdowns = ["Option 1", "Option 2", "Option 3"]
const hardselects = ["Alpha", "Bravo", "Charlie"]
// SOME_NUM is a HARDSELECT_HOOK column — values must come from the dynamic
// validation service (editors/getdynamiccolvals.js -> `other`). Use a subset
// of those valid values so the cells pass validation.
const someNumValues = [
0.00105564761956, 0.00521895988156, 0.0058409725343, 0.00613050395908,
0.01305025071513, 0.01442142483518, 0.02422838845487, 0.0256445333481,
0.02624525922641, 0.02891322459509, 0.02972866503043, 0.03103561933666,
0.03150132113671, 0.03522355064527, 0.03529406247441, 0.03975785711768,
0.04319973664507, 0.04521169189606, 0.04787342951068, 0.04948144222119
]
const rows = []
for (let i = 0; i < n; i++) {
rows.push({
_____DELETE__THIS__RECORD_____: "No",
PRIMARY_KEY_FIELD: i,
SOME_CHAR: "dummy data row " + i,
SOME_DROPDOWN: dropdowns[i % dropdowns.length],
SOME_HARDSELECT: hardselects[i % hardselects.length],
SOME_NUM: someNumValues[i % someNumValues.length],
SOME_DATE: "1960-02-12",
SOME_DATETIME: "1960-01-01 00:00:42",
SOME_TIME: "00:00:42",
SOME_SHORTNUM: (i % 99) + 1,
SOME_BESTNUM: (i % 90) + 10,
// demo columns for the newer DQ rule types
READONLY_COL: "Readonly default",
HIDDEN_COL: "Hidden default",
ROUND_COL: Number((i + 1 + i / 7).toFixed(5)), // rounds on edit
NUMFMT_COL: 1000 + i * 12.5 // shown as EUR
})
}
return rows
}
]
, "cols":
[
{
"NAME": "PRIMARY_KEY_FIELD",
"VARNUM": 1,
"LABEL": "PRIMARY_KEY_FIELD",
"FMTNAME": "",
"DDTYPE": "NUMERIC",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_BESTNUM",
"VARNUM": 9,
"LABEL": "SOME_BESTNUM",
"FMTNAME": "BEST",
"DDTYPE": "NUMERIC",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_CHAR",
"VARNUM": 2,
"LABEL": "SOME_CHAR",
"FMTNAME": "",
"DDTYPE": "CHARACTER",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_DATE",
"VARNUM": 5,
"LABEL": "SOME_DATE",
"FMTNAME": "DATE",
"DDTYPE": "DATE",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_DATETIME",
"VARNUM": 6,
"LABEL": "SOME_DATETIME",
"FMTNAME": "DATETIME",
"DDTYPE": "DATETIME",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_DROPDOWN",
"VARNUM": 3,
"LABEL": "SOME_DROPDOWN",
"FMTNAME": "",
"DDTYPE": "CHARACTER",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_NUM",
"VARNUM": 4,
"LABEL": "SOME_NUM",
"FMTNAME": "",
"DDTYPE": "NUMERIC",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_SHORTNUM",
"VARNUM": 8,
"LABEL": "SOME_SHORTNUM",
"FMTNAME": "",
"DDTYPE": "NUMERIC",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
},
{
"NAME": "SOME_TIME",
"VARNUM": 7,
"LABEL": "SOME_TIME",
"FMTNAME": "TIME",
"DDTYPE": "TIME",
"CLS_RULE": "READ",
"MEMLABEL": "",
"DESC": "",
"LONGDESC": ""
}
]
, "dqdata":
[
{
"BASE_COL": "SOME_DROPDOWN",
"RULE_VALUE": "SOME_DROPDOWN",
"RULE_DATA": "Option 1",
"SELECTBOX_ORDER": 1
},
{
"BASE_COL": "SOME_DROPDOWN",
"RULE_VALUE": "SOME_DROPDOWN",
"RULE_DATA": "Option 2",
"SELECTBOX_ORDER": 2
},
{
"BASE_COL": "SOME_DROPDOWN",
"RULE_VALUE": "SOME_DROPDOWN",
"RULE_DATA": "Option 3",
"SELECTBOX_ORDER": 2
},
{
"BASE_COL": "SOME_DROPDOWN",
"RULE_VALUE": "SOME_DROPDOWN",
"RULE_DATA": "This is a long option. This option is very long. It is optional, though.",
"SELECTBOX_ORDER": 3
}
]
, "dqrules":
[
{
"BASE_COL": "PRIMARY_KEY_FIELD",
"RULE_TYPE": "NOTNULL",
"RULE_VALUE": "",
"X": 0
},
{
"BASE_COL": "SOME_NUM",
"RULE_TYPE": "HARDSELECT_HOOK",
"RULE_VALUE": "services/validations/mpe_x_test.some_num",
"X": 0
}
]
, "dsmeta":
[
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Data Set Name",
"VALUE": "DC996664.MPE_X_TEST"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Observations",
"VALUE": "496"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Member Type",
"VALUE": "DATA"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Variables",
"VALUE": "9"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Engine",
"VALUE": "V9"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Indexes",
"VALUE": "1"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Created",
"VALUE": "09/26/2022 08:24:39"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Integrity Constraints",
"VALUE": "1"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Last Modified",
"VALUE": "09/26/2022 08:24:45"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Observation Length",
"VALUE": "32947"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Protection",
"VALUE": " ."
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Deleted Observations",
"VALUE": "0"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Data Set Type",
"VALUE": " ."
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Compressed",
"VALUE": "CHAR"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Label",
"VALUE": " ."
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Reuse Space",
"VALUE": "NO"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Data Representation",
"VALUE": "WINDOWS_64"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Point to Observations",
"VALUE": "YES"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Encoding",
"VALUE": "wlatin1 Western (Windows)"
},
{
"ODS_TABLE": "ATTRIBUTES",
"NAME": "Sorted",
"VALUE": "NO"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Data Set Page Size",
"VALUE": "262144"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Number of Data Set Pages",
"VALUE": "3"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Index File Page Size",
"VALUE": "4096"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Number of Index File Pages",
"VALUE": "4"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Number of Data Set Repairs",
"VALUE": "0"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "ExtendObsCounter",
"VALUE": "YES"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Filename",
"VALUE": "C:\DataController\DC996664\mpe_x_test.sas7bdat"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Release Created",
"VALUE": "9.0401M7"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Host Created",
"VALUE": "X64_DSRV16"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "Owner Name",
"VALUE": "BUILTIN\Administrators"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "File Size",
"VALUE": " 1MB"
},
{
"ODS_TABLE": "ENGINEHOST",
"NAME": "File Size (bytes)",
"VALUE": "1048576"
}
]
, "maxvarlengths":
[
{
"NAME": "_____DELETE__THIS__RECORD_____",
"MAXLEN": 3
},
{
"NAME": "PRIMARY_KEY_FIELD",
"MAXLEN": 4
},
{
"NAME": "some_char",
"MAXLEN": 591
},
{
"NAME": "some_dropdown",
"MAXLEN": 8
},
{
"NAME": "some_num",
"MAXLEN": 8
},
{
"NAME": "some_date",
"MAXLEN": 10
},
{
"NAME": "some_datetime",
"MAXLEN": 19
},
{
"NAME": "some_time",
"MAXLEN": 8
},
{
"NAME": "some_shortnum",
"MAXLEN": 3
},
{
"NAME": "some_bestnum",
"MAXLEN": 3
}
]
, "query":
[
]
, "sasdata":
[
{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":0 ,"SOME_CHAR":"this is dummy data" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":42 ,"SOME_DATE":"1960-02-12" ,"SOME_DATETIME":"1960-01-01 00:00:42" ,"SOME_TIME":"00:00:42" ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1 ,"SOME_CHAR":"more dummy data" ,"SOME_DROPDOWN":"Option 2" ,"SOME_NUM":42 ,"SOME_DATE":"1960-02-12" ,"SOME_DATETIME":"1960-01-01 00:00:42" ,"SOME_TIME":"00:07:02" ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":2 ,"SOME_CHAR":"even more dummy data" ,"SOME_DROPDOWN":"Option 3" ,"SOME_NUM":42 ,"SOME_DATE":"1960-02-12" ,"SOME_DATETIME":"1960-01-01 00:00:42" ,"SOME_TIME":"00:02:22" ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":3 ,"SOME_CHAR":"It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:" ,"SOME_DROPDOWN":"Option 2" ,"SOME_NUM":1613.001 ,"SOME_DATE":"1961-02-27" ,"SOME_DATETIME":"1960-01-01 00:07:03" ,"SOME_TIME":"00:00:44" ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":4 ,"SOME_CHAR":"if you can fill the unforgiving minute" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":1613.0011235 ,"SOME_DATE":"1971-08-02" ,"SOME_DATETIME":"1973-05-29 06:12:03" ,"SOME_TIME":"00:06:52" ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1010 ,"SOME_CHAR":"10 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.3677867113 ,"SOME_DATE":"1961-03-05" ,"SOME_DATETIME":"1960-01-01 08:16:44" ,"SOME_TIME":"00:00:35" ,"SOME_SHORTNUM":1 ,"SOME_BESTNUM":72 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1011 ,"SOME_CHAR":"11 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8693330497 ,"SOME_DATE":"1961-01-20" ,"SOME_DATETIME":"1960-01-01 01:25:19" ,"SOME_TIME":"00:00:01" ,"SOME_SHORTNUM":6 ,"SOME_BESTNUM":54 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1012 ,"SOME_CHAR":"12 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5432779065 ,"SOME_DATE":"1961-10-06" ,"SOME_DATETIME":"1960-01-01 02:57:35" ,"SOME_TIME":"00:00:35" ,"SOME_SHORTNUM":54 ,"SOME_BESTNUM":62 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1013 ,"SOME_CHAR":"13 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5051939867 ,"SOME_DATE":"1962-02-20" ,"SOME_DATETIME":"1960-01-01 06:47:55" ,"SOME_TIME":"00:00:41" ,"SOME_SHORTNUM":38 ,"SOME_BESTNUM":4 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1014 ,"SOME_CHAR":"14 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0130502507 ,"SOME_DATE":"1960-01-13" ,"SOME_DATETIME":"1960-01-01 03:48:13" ,"SOME_TIME":"00:00:14" ,"SOME_SHORTNUM":92 ,"SOME_BESTNUM":57 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1015 ,"SOME_CHAR":"15 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5822708009 ,"SOME_DATE":"1962-07-12" ,"SOME_DATETIME":"1960-01-01 12:05:18" ,"SOME_TIME":"00:00:54" ,"SOME_SHORTNUM":92 ,"SOME_BESTNUM":80 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1016 ,"SOME_CHAR":"16 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1382724979 ,"SOME_DATE":"1960-08-29" ,"SOME_DATETIME":"1960-01-01 02:48:01" ,"SOME_TIME":"00:00:01" ,"SOME_SHORTNUM":28 ,"SOME_BESTNUM":91 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1017 ,"SOME_CHAR":"17 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.892701324 ,"SOME_DATE":"1961-09-14" ,"SOME_DATETIME":"1960-01-01 07:03:58" ,"SOME_TIME":"00:01:37" ,"SOME_SHORTNUM":91 ,"SOME_BESTNUM":72 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1018 ,"SOME_CHAR":"18 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1852788567 ,"SOME_DATE":"1961-03-08" ,"SOME_DATETIME":"1960-01-01 00:22:48" ,"SOME_TIME":"00:00:32" ,"SOME_SHORTNUM":93 ,"SOME_BESTNUM":79 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1019 ,"SOME_CHAR":"19 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0737551018 ,"SOME_DATE":"1961-01-24" ,"SOME_DATETIME":"1960-01-01 03:14:33" ,"SOME_TIME":"00:00:21" ,"SOME_SHORTNUM":22 ,"SOME_BESTNUM":90 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1020 ,"SOME_CHAR":"20 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7128569939 ,"SOME_DATE":"1961-02-08" ,"SOME_DATETIME":"1960-01-01 01:50:23" ,"SOME_TIME":"00:01:40" ,"SOME_SHORTNUM":65 ,"SOME_BESTNUM":34 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1021 ,"SOME_CHAR":"21 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6706138443 ,"SOME_DATE":"1961-03-09" ,"SOME_DATETIME":"1960-01-01 04:52:55" ,"SOME_TIME":"00:00:13" ,"SOME_SHORTNUM":44 ,"SOME_BESTNUM":97 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1022 ,"SOME_CHAR":"22 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1423215792 ,"SOME_DATE":"1962-07-22" ,"SOME_DATETIME":"1960-01-01 07:25:01" ,"SOME_TIME":"00:01:10" ,"SOME_SHORTNUM":66 ,"SOME_BESTNUM":98 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1023 ,"SOME_CHAR":"23 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1259848066 ,"SOME_DATE":"1962-09-01" ,"SOME_DATETIME":"1960-01-01 09:32:34" ,"SOME_TIME":"00:01:16" ,"SOME_SHORTNUM":44 ,"SOME_BESTNUM":98 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1024 ,"SOME_CHAR":"24 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.3899468637 ,"SOME_DATE":"1961-12-06" ,"SOME_DATETIME":"1960-01-01 06:53:51" ,"SOME_TIME":"00:00:33" ,"SOME_SHORTNUM":30 ,"SOME_BESTNUM":90 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1025 ,"SOME_CHAR":"25 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0310356193 ,"SOME_DATE":"1960-03-01" ,"SOME_DATETIME":"1960-01-01 02:58:07" ,"SOME_TIME":"00:00:27" ,"SOME_SHORTNUM":73 ,"SOME_BESTNUM":59 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1026 ,"SOME_CHAR":"26 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9057884239 ,"SOME_DATE":"1960-10-04" ,"SOME_DATETIME":"1960-01-01 11:17:28" ,"SOME_TIME":"00:00:41" ,"SOME_SHORTNUM":82 ,"SOME_BESTNUM":46 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1027 ,"SOME_CHAR":"27 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5920675856 ,"SOME_DATE":"1962-07-15" ,"SOME_DATETIME":"1960-01-01 03:35:41" ,"SOME_TIME":"00:00:22" ,"SOME_SHORTNUM":46 ,"SOME_BESTNUM":73 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1028 ,"SOME_CHAR":"28 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6580030046 ,"SOME_DATE":"1960-10-08" ,"SOME_DATETIME":"1960-01-01 13:13:30" ,"SOME_TIME":"00:00:40" ,"SOME_SHORTNUM":35 ,"SOME_BESTNUM":40 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1029 ,"SOME_CHAR":"29 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.807042594 ,"SOME_DATE":"1960-12-26" ,"SOME_DATETIME":"1960-01-01 11:57:14" ,"SOME_TIME":"00:00:19" ,"SOME_SHORTNUM":80 ,"SOME_BESTNUM":12 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1030 ,"SOME_CHAR":"30 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8801450408 ,"SOME_DATE":"1961-05-15" ,"SOME_DATETIME":"1960-01-01 10:11:05" ,"SOME_TIME":"00:00:25" ,"SOME_SHORTNUM":70 ,"SOME_BESTNUM":19 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1031 ,"SOME_CHAR":"31 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4150194705 ,"SOME_DATE":"1962-01-27" ,"SOME_DATETIME":"1960-01-01 11:27:09" ,"SOME_TIME":"00:01:04" ,"SOME_SHORTNUM":94 ,"SOME_BESTNUM":48 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1032 ,"SOME_CHAR":"32 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9743401203 ,"SOME_DATE":"1962-01-09" ,"SOME_DATETIME":"1960-01-01 07:44:35" ,"SOME_TIME":"00:01:07" ,"SOME_SHORTNUM":43 ,"SOME_BESTNUM":3 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1033 ,"SOME_CHAR":"33 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.2035595692 ,"SOME_DATE":"1960-09-07" ,"SOME_DATETIME":"1960-01-01 11:52:19" ,"SOME_TIME":"00:00:42" ,"SOME_SHORTNUM":29 ,"SOME_BESTNUM":56 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1034 ,"SOME_CHAR":"34 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6792435556 ,"SOME_DATE":"1960-04-21" ,"SOME_DATETIME":"1960-01-01 07:17:04" ,"SOME_TIME":"00:01:14" ,"SOME_SHORTNUM":68 ,"SOME_BESTNUM":9 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1035 ,"SOME_CHAR":"35 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9494116972 ,"SOME_DATE":"1960-01-19" ,"SOME_DATETIME":"1960-01-01 10:15:38" ,"SOME_TIME":"00:01:16" ,"SOME_SHORTNUM":91 ,"SOME_BESTNUM":10 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1036 ,"SOME_CHAR":"36 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5446134911 ,"SOME_DATE":"1960-10-26" ,"SOME_DATETIME":"1960-01-01 03:55:27" ,"SOME_TIME":"00:01:24" ,"SOME_SHORTNUM":72 ,"SOME_BESTNUM":36 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1037 ,"SOME_CHAR":"37 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.458775894 ,"SOME_DATE":"1960-11-21" ,"SOME_DATETIME":"1960-01-01 13:34:37" ,"SOME_TIME":"00:01:35" ,"SOME_SHORTNUM":97 ,"SOME_BESTNUM":32 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1038 ,"SOME_CHAR":"38 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1537194239 ,"SOME_DATE":"1961-05-06" ,"SOME_DATETIME":"1960-01-01 06:14:13" ,"SOME_TIME":"00:00:29" ,"SOME_SHORTNUM":60 ,"SOME_BESTNUM":98 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1039 ,"SOME_CHAR":"39 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4935002562 ,"SOME_DATE":"1960-06-05" ,"SOME_DATETIME":"1960-01-01 06:59:42" ,"SOME_TIME":"00:00:45" ,"SOME_SHORTNUM":95 ,"SOME_BESTNUM":55 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1040 ,"SOME_CHAR":"40 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.124728859 ,"SOME_DATE":"1961-03-09" ,"SOME_DATETIME":"1960-01-01 03:03:06" ,"SOME_TIME":"00:01:23" ,"SOME_SHORTNUM":35 ,"SOME_BESTNUM":79 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1041 ,"SOME_CHAR":"41 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.2794422001 ,"SOME_DATE":"1962-07-06" ,"SOME_DATETIME":"1960-01-01 05:29:26" ,"SOME_TIME":"00:00:51" ,"SOME_SHORTNUM":86 ,"SOME_BESTNUM":66 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1042 ,"SOME_CHAR":"42 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7030775499 ,"SOME_DATE":"1960-08-11" ,"SOME_DATETIME":"1960-01-01 12:11:24" ,"SOME_TIME":"00:00:38" ,"SOME_SHORTNUM":86 ,"SOME_BESTNUM":97 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1043 ,"SOME_CHAR":"43 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0701107537 ,"SOME_DATE":"1961-01-29" ,"SOME_DATETIME":"1960-01-01 03:44:09" ,"SOME_TIME":"00:00:03" ,"SOME_SHORTNUM":25 ,"SOME_BESTNUM":8 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1044 ,"SOME_CHAR":"44 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6423292927 ,"SOME_DATE":"1962-01-15" ,"SOME_DATETIME":"1960-01-01 00:57:07" ,"SOME_TIME":"00:00:09" ,"SOME_SHORTNUM":97 ,"SOME_BESTNUM":37 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1045 ,"SOME_CHAR":"45 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7206447743 ,"SOME_DATE":"1961-10-14" ,"SOME_DATETIME":"1960-01-01 12:25:32" ,"SOME_TIME":"00:00:07" ,"SOME_SHORTNUM":58 ,"SOME_BESTNUM":58 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1046 ,"SOME_CHAR":"46 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0431997366 ,"SOME_DATE":"1960-09-12" ,"SOME_DATETIME":"1960-01-01 05:12:57" ,"SOME_TIME":"00:01:35" ,"SOME_SHORTNUM":17 ,"SOME_BESTNUM":8 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1047 ,"SOME_CHAR":"47 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.3704071368 ,"SOME_DATE":"1960-07-01" ,"SOME_DATETIME":"1960-01-01 02:44:37" ,"SOME_TIME":"00:00:06" ,"SOME_SHORTNUM":45 ,"SOME_BESTNUM":26 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1048 ,"SOME_CHAR":"48 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.654417035 ,"SOME_DATE":"1961-05-04" ,"SOME_DATETIME":"1960-01-01 01:23:07" ,"SOME_TIME":"00:01:38" ,"SOME_SHORTNUM":41 ,"SOME_BESTNUM":13 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1049 ,"SOME_CHAR":"49 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1300212565 ,"SOME_DATE":"1961-01-06" ,"SOME_DATETIME":"1960-01-01 05:27:29" ,"SOME_TIME":"00:01:21" ,"SOME_SHORTNUM":37 ,"SOME_BESTNUM":66 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1050 ,"SOME_CHAR":"50 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0058409725 ,"SOME_DATE":"1960-07-23" ,"SOME_DATETIME":"1960-01-01 00:04:24" ,"SOME_TIME":"00:00:40" ,"SOME_SHORTNUM":15 ,"SOME_BESTNUM":32 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1051 ,"SOME_CHAR":"51 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7239382587 ,"SOME_DATE":"1960-06-09" ,"SOME_DATETIME":"1960-01-01 03:15:09" ,"SOME_TIME":"00:00:04" ,"SOME_SHORTNUM":42 ,"SOME_BESTNUM":82 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1052 ,"SOME_CHAR":"52 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8319003712 ,"SOME_DATE":"1960-08-13" ,"SOME_DATETIME":"1960-01-01 07:38:35" ,"SOME_TIME":"00:00:36" ,"SOME_SHORTNUM":69 ,"SOME_BESTNUM":81 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1053 ,"SOME_CHAR":"53 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5030828875 ,"SOME_DATE":"1961-06-22" ,"SOME_DATETIME":"1960-01-01 11:25:29" ,"SOME_TIME":"00:00:53" ,"SOME_SHORTNUM":39 ,"SOME_BESTNUM":75 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1054 ,"SOME_CHAR":"54 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7148045514 ,"SOME_DATE":"1960-08-26" ,"SOME_DATETIME":"1960-01-01 10:10:09" ,"SOME_TIME":"00:00:39" ,"SOME_SHORTNUM":6 ,"SOME_BESTNUM":4 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1055 ,"SOME_CHAR":"55 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8557945787 ,"SOME_DATE":"1960-10-19" ,"SOME_DATETIME":"1960-01-01 02:17:32" ,"SOME_TIME":"00:00:08" ,"SOME_SHORTNUM":93 ,"SOME_BESTNUM":36 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1056 ,"SOME_CHAR":"56 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9700463307 ,"SOME_DATE":"1962-07-11" ,"SOME_DATETIME":"1960-01-01 11:18:41" ,"SOME_TIME":"00:00:51" ,"SOME_SHORTNUM":25 ,"SOME_BESTNUM":35 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1057 ,"SOME_CHAR":"57 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9380399426 ,"SOME_DATE":"1961-06-26" ,"SOME_DATETIME":"1960-01-01 13:15:13" ,"SOME_TIME":"00:00:52" ,"SOME_SHORTNUM":57 ,"SOME_BESTNUM":66 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1058 ,"SOME_CHAR":"58 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8484499486 ,"SOME_DATE":"1960-06-02" ,"SOME_DATETIME":"1960-01-01 01:14:51" ,"SOME_TIME":"00:00:00" ,"SOME_SHORTNUM":80 ,"SOME_BESTNUM":58 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1059 ,"SOME_CHAR":"59 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1415707628 ,"SOME_DATE":"1961-07-28" ,"SOME_DATETIME":"1960-01-01 06:33:16" ,"SOME_TIME":"00:00:58" ,"SOME_SHORTNUM":11 ,"SOME_BESTNUM":32 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1060 ,"SOME_CHAR":"60 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.282674513 ,"SOME_DATE":"1962-03-27" ,"SOME_DATETIME":"1960-01-01 00:25:37" ,"SOME_TIME":"00:00:56" ,"SOME_SHORTNUM":79 ,"SOME_BESTNUM":58 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1061 ,"SOME_CHAR":"61 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.372728008 ,"SOME_DATE":"1962-01-04" ,"SOME_DATETIME":"1960-01-01 05:07:43" ,"SOME_TIME":"00:01:00" ,"SOME_SHORTNUM":86 ,"SOME_BESTNUM":92 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1062 ,"SOME_CHAR":"62 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9517337316 ,"SOME_DATE":"1961-08-29" ,"SOME_DATETIME":"1960-01-01 02:40:05" ,"SOME_TIME":"00:00:05" ,"SOME_SHORTNUM":30 ,"SOME_BESTNUM":93 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1063 ,"SOME_CHAR":"63 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0967498683 ,"SOME_DATE":"1962-02-17" ,"SOME_DATETIME":"1960-01-01 07:30:41" ,"SOME_TIME":"00:00:29" ,"SOME_SHORTNUM":90 ,"SOME_BESTNUM":82 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1064 ,"SOME_CHAR":"64 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0540671353 ,"SOME_DATE":"1961-05-26" ,"SOME_DATETIME":"1960-01-01 13:13:43" ,"SOME_TIME":"00:00:08" ,"SOME_SHORTNUM":88 ,"SOME_BESTNUM":45 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1065 ,"SOME_CHAR":"65 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6461636464 ,"SOME_DATE":"1962-01-27" ,"SOME_DATETIME":"1960-01-01 02:56:41" ,"SOME_TIME":"00:00:19" ,"SOME_SHORTNUM":41 ,"SOME_BESTNUM":38 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1066 ,"SOME_CHAR":"66 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9053011983 ,"SOME_DATE":"1960-10-02" ,"SOME_DATETIME":"1960-01-01 03:35:49" ,"SOME_TIME":"00:01:04" ,"SOME_SHORTNUM":68 ,"SOME_BESTNUM":39 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1067 ,"SOME_CHAR":"67 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.977525881 ,"SOME_DATE":"1962-07-19" ,"SOME_DATETIME":"1960-01-01 05:53:20" ,"SOME_TIME":"00:00:28" ,"SOME_SHORTNUM":28 ,"SOME_BESTNUM":34 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1068 ,"SOME_CHAR":"68 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.2165553161 ,"SOME_DATE":"1960-05-13" ,"SOME_DATETIME":"1960-01-01 01:44:02" ,"SOME_TIME":"00:01:12" ,"SOME_SHORTNUM":63 ,"SOME_BESTNUM":23 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1069 ,"SOME_CHAR":"69 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.2248352795 ,"SOME_DATE":"1961-05-09" ,"SOME_DATETIME":"1960-01-01 00:04:33" ,"SOME_TIME":"00:00:09" ,"SOME_SHORTNUM":26 ,"SOME_BESTNUM":93 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1070 ,"SOME_CHAR":"70 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1386283367 ,"SOME_DATE":"1962-05-18" ,"SOME_DATETIME":"1960-01-01 03:32:00" ,"SOME_TIME":"00:01:36" ,"SOME_SHORTNUM":83 ,"SOME_BESTNUM":89 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1071 ,"SOME_CHAR":"71 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9337331415 ,"SOME_DATE":"1961-05-16" ,"SOME_DATETIME":"1960-01-01 13:46:54" ,"SOME_TIME":"00:00:47" ,"SOME_SHORTNUM":27 ,"SOME_BESTNUM":56 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1072 ,"SOME_CHAR":"72 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0352235506 ,"SOME_DATE":"1961-06-06" ,"SOME_DATETIME":"1960-01-01 09:09:20" ,"SOME_TIME":"00:01:16" ,"SOME_SHORTNUM":7 ,"SOME_BESTNUM":27 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1073 ,"SOME_CHAR":"73 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.3206662695 ,"SOME_DATE":"1960-03-13" ,"SOME_DATETIME":"1960-01-01 10:38:11" ,"SOME_TIME":"00:01:08" ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":50 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1074 ,"SOME_CHAR":"74 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4610861705 ,"SOME_DATE":"1961-08-31" ,"SOME_DATETIME":"1960-01-01 09:35:41" ,"SOME_TIME":"00:01:08" ,"SOME_SHORTNUM":54 ,"SOME_BESTNUM":68 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1075 ,"SOME_CHAR":"75 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4527745622 ,"SOME_DATE":"1962-01-16" ,"SOME_DATETIME":"1960-01-01 06:49:27" ,"SOME_TIME":"00:00:45" ,"SOME_SHORTNUM":96 ,"SOME_BESTNUM":63 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1076 ,"SOME_CHAR":"76 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.3581244058 ,"SOME_DATE":"1960-05-16" ,"SOME_DATETIME":"1960-01-01 00:56:40" ,"SOME_TIME":"00:01:13" ,"SOME_SHORTNUM":72 ,"SOME_BESTNUM":24 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1077 ,"SOME_CHAR":"77 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8939921334 ,"SOME_DATE":"1961-01-21" ,"SOME_DATETIME":"1960-01-01 09:16:31" ,"SOME_TIME":"00:01:15" ,"SOME_SHORTNUM":88 ,"SOME_BESTNUM":69 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1078 ,"SOME_CHAR":"78 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.2445727066 ,"SOME_DATE":"1960-12-22" ,"SOME_DATETIME":"1960-01-01 03:11:14" ,"SOME_TIME":"00:01:37" ,"SOME_SHORTNUM":88 ,"SOME_BESTNUM":32 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1079 ,"SOME_CHAR":"79 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9683029465 ,"SOME_DATE":"1961-08-14" ,"SOME_DATETIME":"1960-01-01 04:45:43" ,"SOME_TIME":"00:01:09" ,"SOME_SHORTNUM":51 ,"SOME_BESTNUM":60 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1080 ,"SOME_CHAR":"80 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1303541368 ,"SOME_DATE":"1962-02-28" ,"SOME_DATETIME":"1960-01-01 02:14:50" ,"SOME_TIME":"00:00:21" ,"SOME_SHORTNUM":79 ,"SOME_BESTNUM":87 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1081 ,"SOME_CHAR":"81 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7656979653 ,"SOME_DATE":"1961-08-03" ,"SOME_DATETIME":"1960-01-01 06:49:50" ,"SOME_TIME":"00:01:31" ,"SOME_SHORTNUM":58 ,"SOME_BESTNUM":30 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1082 ,"SOME_CHAR":"82 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1855629674 ,"SOME_DATE":"1960-12-16" ,"SOME_DATETIME":"1960-01-01 06:27:21" ,"SOME_TIME":"00:00:33" ,"SOME_SHORTNUM":1 ,"SOME_BESTNUM":72 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1083 ,"SOME_CHAR":"83 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4782178642 ,"SOME_DATE":"1961-04-16" ,"SOME_DATETIME":"1960-01-01 08:05:23" ,"SOME_TIME":"00:01:10" ,"SOME_SHORTNUM":0 ,"SOME_BESTNUM":1 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1084 ,"SOME_CHAR":"84 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1670272132 ,"SOME_DATE":"1962-06-21" ,"SOME_DATETIME":"1960-01-01 13:43:20" ,"SOME_TIME":"00:00:27" ,"SOME_SHORTNUM":53 ,"SOME_BESTNUM":6 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1085 ,"SOME_CHAR":"85 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6068249189 ,"SOME_DATE":"1960-05-21" ,"SOME_DATETIME":"1960-01-01 11:05:11" ,"SOME_TIME":"00:00:08" ,"SOME_SHORTNUM":17 ,"SOME_BESTNUM":68 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1086 ,"SOME_CHAR":"86 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0936049917 ,"SOME_DATE":"1962-07-20" ,"SOME_DATETIME":"1960-01-01 07:16:09" ,"SOME_TIME":"00:00:46" ,"SOME_SHORTNUM":73 ,"SOME_BESTNUM":37 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1087 ,"SOME_CHAR":"87 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6538249178 ,"SOME_DATE":"1960-04-24" ,"SOME_DATETIME":"1960-01-01 02:06:54" ,"SOME_TIME":"00:00:59" ,"SOME_SHORTNUM":95 ,"SOME_BESTNUM":32 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1088 ,"SOME_CHAR":"88 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8846158562 ,"SOME_DATE":"1961-11-19" ,"SOME_DATETIME":"1960-01-01 05:35:27" ,"SOME_TIME":"00:01:01" ,"SOME_SHORTNUM":87 ,"SOME_BESTNUM":30 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1089 ,"SOME_CHAR":"89 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1578208316 ,"SOME_DATE":"1961-03-03" ,"SOME_DATETIME":"1960-01-01 09:02:02" ,"SOME_TIME":"00:00:23" ,"SOME_SHORTNUM":60 ,"SOME_BESTNUM":53 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1090 ,"SOME_CHAR":"90 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4225753753 ,"SOME_DATE":"1960-03-19" ,"SOME_DATETIME":"1960-01-01 12:14:04" ,"SOME_TIME":"00:01:00" ,"SOME_SHORTNUM":57 ,"SOME_BESTNUM":64 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1091 ,"SOME_CHAR":"91 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6598943354 ,"SOME_DATE":"1961-09-17" ,"SOME_DATETIME":"1960-01-01 03:03:13" ,"SOME_TIME":"00:01:00" ,"SOME_SHORTNUM":41 ,"SOME_BESTNUM":28 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1092 ,"SOME_CHAR":"92 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6293501689 ,"SOME_DATE":"1961-10-18" ,"SOME_DATETIME":"1960-01-01 00:21:13" ,"SOME_TIME":"00:01:11" ,"SOME_SHORTNUM":64 ,"SOME_BESTNUM":7 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1093 ,"SOME_CHAR":"93 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4378844986 ,"SOME_DATE":"1961-06-24" ,"SOME_DATETIME":"1960-01-01 10:20:39" ,"SOME_TIME":"00:00:27" ,"SOME_SHORTNUM":30 ,"SOME_BESTNUM":78 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1094 ,"SOME_CHAR":"94 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9838584969 ,"SOME_DATE":"1962-05-25" ,"SOME_DATETIME":"1960-01-01 02:59:06" ,"SOME_TIME":"00:00:59" ,"SOME_SHORTNUM":48 ,"SOME_BESTNUM":98 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1095 ,"SOME_CHAR":"95 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.089252377 ,"SOME_DATE":"1961-06-16" ,"SOME_DATETIME":"1960-01-01 04:54:20" ,"SOME_TIME":"00:00:10" ,"SOME_SHORTNUM":75 ,"SOME_BESTNUM":33 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1096 ,"SOME_CHAR":"96 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4578205154 ,"SOME_DATE":"1960-01-20" ,"SOME_DATETIME":"1960-01-01 10:36:00" ,"SOME_TIME":"00:00:41" ,"SOME_SHORTNUM":14 ,"SOME_BESTNUM":17 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1097 ,"SOME_CHAR":"97 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5863271587 ,"SOME_DATE":"1962-04-20" ,"SOME_DATETIME":"1960-01-01 11:14:11" ,"SOME_TIME":"00:01:28" ,"SOME_SHORTNUM":66 ,"SOME_BESTNUM":84 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1098 ,"SOME_CHAR":"98 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.2994232058 ,"SOME_DATE":"1960-07-04" ,"SOME_DATETIME":"1960-01-01 08:15:41" ,"SOME_TIME":"00:01:28" ,"SOME_SHORTNUM":99 ,"SOME_BESTNUM":85 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":1099 ,"SOME_CHAR":"99 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.0981378053 ,"SOME_DATE":"1960-02-05" ,"SOME_DATETIME":"1960-01-01 11:10:11" ,"SOME_TIME":"00:00:43" ,"SOME_SHORTNUM":23 ,"SOME_BESTNUM":65 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":10100 ,"SOME_CHAR":"100 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9829722652 ,"SOME_DATE":"1960-02-01" ,"SOME_DATETIME":"1960-01-01 05:45:06" ,"SOME_TIME":"00:01:16" ,"SOME_SHORTNUM":28 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":10101 ,"SOME_CHAR":"101 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4540794913 ,"SOME_DATE":"1962-08-03" ,"SOME_DATETIME":"1960-01-01 09:27:03" ,"SOME_TIME":"00:01:10" ,"SOME_SHORTNUM":42 ,"SOME_BESTNUM":44 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":10102 ,"SOME_CHAR":"102 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.8452174369 ,"SOME_DATE":"1960-10-02" ,"SOME_DATETIME":"1960-01-01 03:08:47" ,"SOME_TIME":"00:00:23" ,"SOME_SHORTNUM":10 ,"SOME_BESTNUM":14 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":10103 ,"SOME_CHAR":"103 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5904919606 ,"SOME_DATE":"1960-07-30" ,"SOME_DATETIME":"1960-01-01 13:09:58" ,"SOME_TIME":"00:00:10" ,"SOME_SHORTNUM":68 ,"SOME_BESTNUM":53 }
,{"_____DELETE__THIS__RECORD_____":"No" ,"PRIMARY_KEY_FIELD":10104 ,"SOME_CHAR":"104 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7083388677 ,"SOME_DATE":"1960-07-24" ,"SOME_DATETIME":"1960-01-01 05:49:50" ,"SOME_TIME":"00:01:29" ,"SOME_SHORTNUM":84 ,"SOME_BESTNUM":33 }
]
, "$sasdata":{"vars":{
"_____DELETE__THIS__RECORD_____" :{"format":"$3." ,"label":"_____DELETE__THIS__RECORD_____" ,"length":"3" ,"type":"char" }
,"PRIMARY_KEY_FIELD" :{"format":"best." ,"label":"PRIMARY_KEY_FIELD" ,"length":"8" ,"type":"num" }
,"SOME_CHAR" :{"format":"$32767." ,"label":"SOME_CHAR" ,"length":"32767" ,"type":"char" }
,"SOME_DROPDOWN" :{"format":"$128." ,"label":"SOME_DROPDOWN" ,"length":"128" ,"type":"char" }
,"SOME_NUM" :{"format":"best." ,"label":"SOME_NUM" ,"length":"8" ,"type":"num" }
,"SOME_DATE" :{"format":"$200." ,"label":"SOME_DATE" ,"length":"200" ,"type":"char" }
,"SOME_DATETIME" :{"format":"$200." ,"label":"SOME_DATETIME" ,"length":"200" ,"type":"char" }
,"SOME_TIME" :{"format":"$200." ,"label":"SOME_TIME" ,"length":"200" ,"type":"char" }
,"SOME_SHORTNUM" :{"format":"best." ,"label":"SOME_SHORTNUM" ,"length":"4" ,"type":"num" }
,"SOME_BESTNUM" :{"format":"BEST." ,"label":"SOME_BESTNUM" ,"length":"8" ,"type":"num" }
}}
, "sasparams":
[
{
"COLHEADERS": "_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM",
"FILTER_TEXT": "",
"PKCNT": 1,
"PK": "PRIMARY_KEY_FIELD",
"DTVARS": " SOME_DATE",
"DTTMVARS": " SOME_DATETIME",
"TMVARS": " SOME_TIME",
"COLTYPE": "{\\"data\\":\\"_____DELETE__THIS__RECORD_____\\",\\"type\\":\\"dropdown\\",\\"source\\":[\\"No\\",\\"Yes\\"]},{\\"data\\":\\"PRIMARY_KEY_FIELD\\",\\"type\\":\\"numeric\\",\\"format\\":\\"0\\"},{\\"data\\":\\"SOME_CHAR\\"},{\\"data\\":\\"SOME_DROPDOWN\\"},{\\"data\\":\\"SOME_NUM\\",\\"type\\":\\"numeric\\",\\"format\\":\\"0\\"},{\\"data\\":\\"SOME_DATE\\",\\"type\\":\\"date\\",\\"dateFormat\\":\\"YYYY-MM-DD\\",\\"correctFormat\\":\\"true\\"},{\\"data\\":\\"SOME_DATETIME\\",\\"type\\":\\"date\\",\\"dateFormat\\":\\"YYYY-MM-DD HH:mm:ss\\",\\"correctFormat\\":\\"true\\"},{\\"data\\":\\"SOME_TIME\\",\\"type\\":\\"time\\",\\"timeFormat\\":\\"HH:mm:ss\\",\\"correctFormat\\":\\"true\\"},{\\"data\\":\\"SOME_SHORTNUM\\",\\"type\\":\\"numeric\\",\\"format\\":\\"0\\"},{\\"data\\":\\"SOME_BESTNUM\\",\\"type\\":\\"numeric\\",\\"format\\":\\"0\\"}",
"LOADTYPE": "UPDATE",
"RK_FLAG": 0,
"CLS_FLAG": 0
const data = {
SYSDATE: "26SEP22",
SYSTIME: "08:30",
approvers: [
{ PERSONNAME: "sasdemo", EMAIL: "sasdemo", USERID: "sasdemo" }
],
cols: [
{
NAME: "PRIMARY_KEY_FIELD",
VARNUM: 1,
LABEL: "PRIMARY_KEY_FIELD",
FMTNAME: "",
DDTYPE: "NUMERIC",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_BESTNUM",
VARNUM: 9,
LABEL: "SOME_BESTNUM",
FMTNAME: "BEST",
DDTYPE: "NUMERIC",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_CHAR",
VARNUM: 2,
LABEL: "SOME_CHAR",
FMTNAME: "",
DDTYPE: "CHARACTER",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_DATE",
VARNUM: 5,
LABEL: "SOME_DATE",
FMTNAME: "DATE",
DDTYPE: "DATE",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_DATETIME",
VARNUM: 6,
LABEL: "SOME_DATETIME",
FMTNAME: "DATETIME",
DDTYPE: "DATETIME",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_DROPDOWN",
VARNUM: 3,
LABEL: "SOME_DROPDOWN",
FMTNAME: "",
DDTYPE: "CHARACTER",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_HARDSELECT",
VARNUM: 10,
LABEL: "SOME_HARDSELECT",
FMTNAME: "",
DDTYPE: "CHARACTER",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_NUM",
VARNUM: 4,
LABEL: "SOME_NUM",
FMTNAME: "",
DDTYPE: "NUMERIC",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_SHORTNUM",
VARNUM: 8,
LABEL: "SOME_SHORTNUM",
FMTNAME: "",
DDTYPE: "NUMERIC",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "SOME_TIME",
VARNUM: 7,
LABEL: "SOME_TIME",
FMTNAME: "TIME",
DDTYPE: "TIME",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "",
LONGDESC: ""
},
{
NAME: "READONLY_COL",
VARNUM: 11,
LABEL: "READONLY_COL",
FMTNAME: "",
DDTYPE: "CHARACTER",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "Read-only: default value inserted on add-row, not editable",
LONGDESC: ""
},
{
NAME: "HIDDEN_COL",
VARNUM: 12,
LABEL: "HIDDEN_COL",
FMTNAME: "",
DDTYPE: "CHARACTER",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "Hidden: invisible in grid but submitted; default on add-row",
LONGDESC: ""
},
{
NAME: "ROUND_COL",
VARNUM: 13,
LABEL: "ROUND_COL",
FMTNAME: "",
DDTYPE: "NUMERIC",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "Round: edited values rounded Excel-style to 2 decimals",
LONGDESC: ""
},
{
NAME: "NUMFMT_COL",
VARNUM: 14,
LABEL: "NUMFMT_COL",
FMTNAME: "",
DDTYPE: "NUMERIC",
CLS_RULE: "READ",
MEMLABEL: "",
DESC: "Number format: displayed as EUR currency (value unchanged)",
LONGDESC: ""
}
],
dqdata: [
{ BASE_COL: "SOME_DROPDOWN", RULE_VALUE: "SOME_DROPDOWN", RULE_DATA: "Option 1", SELECTBOX_ORDER: 1 },
{ BASE_COL: "SOME_DROPDOWN", RULE_VALUE: "SOME_DROPDOWN", RULE_DATA: "Option 2", SELECTBOX_ORDER: 2 },
{ BASE_COL: "SOME_DROPDOWN", RULE_VALUE: "SOME_DROPDOWN", RULE_DATA: "Option 3", SELECTBOX_ORDER: 2 },
{
BASE_COL: "SOME_DROPDOWN",
RULE_VALUE: "SOME_DROPDOWN",
RULE_DATA: "This is a long option. This option is very long. It is optional, though.",
SELECTBOX_ORDER: 3
},
{ BASE_COL: "SOME_HARDSELECT", RULE_VALUE: "SOME_HARDSELECT", RULE_DATA: "Alpha", SELECTBOX_ORDER: 1 },
{ BASE_COL: "SOME_HARDSELECT", RULE_VALUE: "SOME_HARDSELECT", RULE_DATA: "Bravo", SELECTBOX_ORDER: 2 },
{ BASE_COL: "SOME_HARDSELECT", RULE_VALUE: "SOME_HARDSELECT", RULE_DATA: "Charlie", SELECTBOX_ORDER: 3 }
],
dqrules: [
{ BASE_COL: "PRIMARY_KEY_FIELD", RULE_TYPE: "NOTNULL", RULE_VALUE: "", X: 0 },
{ BASE_COL: "SOME_NUM", RULE_TYPE: "HARDSELECT_HOOK", RULE_VALUE: "services/validations/mpe_x_test.some_num", X: 0 },
{ BASE_COL: "SOME_HARDSELECT", RULE_TYPE: "HARDSELECT", RULE_VALUE: "", X: 0 },
{ BASE_COL: "READONLY_COL", RULE_TYPE: "READONLY", RULE_VALUE: "Readonly default", X: 0 },
{ BASE_COL: "HIDDEN_COL", RULE_TYPE: "HIDDEN", RULE_VALUE: "Hidden default", X: 0 },
{ BASE_COL: "ROUND_COL", RULE_TYPE: "ROUND", RULE_VALUE: "2", X: 0 },
{ BASE_COL: "NUMFMT_COL", RULE_TYPE: "NUMBER_FORMAT", RULE_VALUE: '{"style":"currency","currency":"EUR"}', X: 0 }
],
dsmeta: [
{ ODS_TABLE: "ATTRIBUTES", NAME: "Data Set Name", VALUE: "DC996664.MPE_X_TEST" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Observations", VALUE: "496" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Member Type", VALUE: "DATA" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Variables", VALUE: "9" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Engine", VALUE: "V9" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Indexes", VALUE: "1" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Created", VALUE: "09/26/2022 08:24:39" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Integrity Constraints", VALUE: "1" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Last Modified", VALUE: "09/26/2022 08:24:45" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Observation Length", VALUE: "32947" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Protection", VALUE: " ." },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Deleted Observations", VALUE: "0" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Data Set Type", VALUE: " ." },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Compressed", VALUE: "CHAR" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Label", VALUE: " ." },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Reuse Space", VALUE: "NO" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Data Representation", VALUE: "WINDOWS_64" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Point to Observations", VALUE: "YES" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Encoding", VALUE: "wlatin1 Western (Windows)" },
{ ODS_TABLE: "ATTRIBUTES", NAME: "Sorted", VALUE: "NO" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Data Set Page Size", VALUE: "262144" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Number of Data Set Pages", VALUE: "3" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Index File Page Size", VALUE: "4096" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Number of Index File Pages", VALUE: "4" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Number of Data Set Repairs", VALUE: "0" },
{ ODS_TABLE: "ENGINEHOST", NAME: "ExtendObsCounter", VALUE: "YES" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Filename", VALUE: "C:DataControllerDC996664mpe_x_test.sas7bdat" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Release Created", VALUE: "9.0401M7" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Host Created", VALUE: "X64_DSRV16" },
{ ODS_TABLE: "ENGINEHOST", NAME: "Owner Name", VALUE: "BUILTINAdministrators" },
{ ODS_TABLE: "ENGINEHOST", NAME: "File Size", VALUE: " 1MB" },
{ ODS_TABLE: "ENGINEHOST", NAME: "File Size (bytes)", VALUE: "1048576" }
],
maxvarlengths: [
{ NAME: "_____DELETE__THIS__RECORD_____", MAXLEN: 3 },
{ NAME: "PRIMARY_KEY_FIELD", MAXLEN: 4 },
{ NAME: "some_char", MAXLEN: 591 },
{ NAME: "some_dropdown", MAXLEN: 8 },
{ NAME: "some_hardselect", MAXLEN: 7 },
{ NAME: "some_num", MAXLEN: 8 },
{ NAME: "some_date", MAXLEN: 10 },
{ NAME: "some_datetime", MAXLEN: 19 },
{ NAME: "some_time", MAXLEN: 8 },
{ NAME: "some_shortnum", MAXLEN: 3 },
{ NAME: "some_bestnum", MAXLEN: 3 },
{ NAME: "readonly_col", MAXLEN: 16 },
{ NAME: "hidden_col", MAXLEN: 14 },
{ NAME: "round_col", MAXLEN: 8 },
{ NAME: "numfmt_col", MAXLEN: 8 }
],
query: [],
sasdata: makeRows(100),
$sasdata: {
vars: {
_____DELETE__THIS__RECORD_____: { format: "$3.", label: "_____DELETE__THIS__RECORD_____", length: "3", type: "char" },
PRIMARY_KEY_FIELD: { format: "best.", label: "PRIMARY_KEY_FIELD", length: "8", type: "num" },
SOME_CHAR: { format: "$32767.", label: "SOME_CHAR", length: "32767", type: "char" },
SOME_DROPDOWN: { format: "$128.", label: "SOME_DROPDOWN", length: "128", type: "char" },
SOME_HARDSELECT: { format: "$128.", label: "SOME_HARDSELECT", length: "128", type: "char" },
SOME_NUM: { format: "best.", label: "SOME_NUM", length: "8", type: "num" },
SOME_DATE: { format: "$200.", label: "SOME_DATE", length: "200", type: "char" },
SOME_DATETIME: { format: "$200.", label: "SOME_DATETIME", length: "200", type: "char" },
SOME_TIME: { format: "$200.", label: "SOME_TIME", length: "200", type: "char" },
SOME_SHORTNUM: { format: "best.", label: "SOME_SHORTNUM", length: "4", type: "num" },
SOME_BESTNUM: { format: "BEST.", label: "SOME_BESTNUM", length: "8", type: "num" },
READONLY_COL: { format: "$200.", label: "READONLY_COL", length: "200", type: "char" },
HIDDEN_COL: { format: "$200.", label: "HIDDEN_COL", length: "200", type: "char" },
ROUND_COL: { format: "best.", label: "ROUND_COL", length: "8", type: "num" },
NUMFMT_COL: { format: "best.", label: "NUMFMT_COL", length: "8", type: "num" }
}
},
sasparams: [
{
COLHEADERS: "_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_HARDSELECT,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM,READONLY_COL,HIDDEN_COL,ROUND_COL,NUMFMT_COL",
FILTER_TEXT: "",
PKCNT: 1,
PK: "PRIMARY_KEY_FIELD",
DTVARS: " SOME_DATE",
DTTMVARS: " SOME_DATETIME",
TMVARS: " SOME_TIME",
COLTYPE: "{\"data\":\"_____DELETE__THIS__RECORD_____\",\"type\":\"dropdown\",\"source\":[\"No\",\"Yes\"]},{\"data\":\"PRIMARY_KEY_FIELD\",\"type\":\"numeric\",\"format\":\"0\"},{\"data\":\"SOME_CHAR\"},{\"data\":\"SOME_DROPDOWN\"},{\"data\":\"SOME_HARDSELECT\"},{\"data\":\"SOME_NUM\",\"type\":\"numeric\",\"format\":\"0\"},{\"data\":\"SOME_DATE\",\"type\":\"date\",\"dateFormat\":\"YYYY-MM-DD\",\"correctFormat\":\"true\"},{\"data\":\"SOME_DATETIME\",\"type\":\"date\",\"dateFormat\":\"YYYY-MM-DD HH:mm:ss\",\"correctFormat\":\"true\"},{\"data\":\"SOME_TIME\",\"type\":\"time\",\"timeFormat\":\"HH:mm:ss\",\"correctFormat\":\"true\"},{\"data\":\"SOME_SHORTNUM\",\"type\":\"numeric\",\"format\":\"0\"},{\"data\":\"SOME_BESTNUM\",\"type\":\"numeric\",\"format\":\"0\"},{\"data\":\"READONLY_COL\"},{\"data\":\"HIDDEN_COL\"},{\"data\":\"ROUND_COL\",\"type\":\"numeric\",\"format\":\"0\"},{\"data\":\"NUMFMT_COL\",\"type\":\"numeric\",\"format\":\"0\"}",
LOADTYPE: "UPDATE",
RK_FLAG: 0,
CLS_FLAG: 0
}
],
xl_rules: [],
_DEBUG: "",
_METAUSER: "sasdemo@SAS",
_METAPERSON: "sasdemo",
_PROGRAM: "/Projects/app/dc/services/editors/getdata",
AUTOEXEC: "D%3A%5Copt%5Csasinside%5CConfig%5CLev1%5CSASApp%5CStoredProcessServer%5Cautoexec.sas",
MF_GETUSER: "sasdemo",
SYSCC: "0",
SYSENCODING: "wlatin1",
SYSERRORTEXT: "",
SYSHOSTNAME: "SAS",
SYSPROCESSID: "41DD8056A491DB23409E940000000000",
SYSPROCESSMODE: "SAS Stored Process Server",
SYSPROCESSNAME: "",
SYSJOBID: "27448",
SYSSCPL: "X64_DSRV16",
SYSSITE: "123",
SYSUSERID: "sassrv",
SYSVLONG: "9.04.01M7P080520",
SYSWARNINGTEXT: "ENCODING option ignored for files opened with RECFM=N.",
END_DTTM: "2022-09-26T08:30:13.853000",
MEMSIZE: "46GB"
}
]
, "xl_rules":
[
]
,"_DEBUG" : ""
,"_METAUSER": "sasdemo@SAS"
,"_METAPERSON": "sasdemo"
,"_PROGRAM" : "/Projects/app/dc/services/editors/getdata"
,"AUTOEXEC" : "D%3A%5Copt%5Csasinside%5CConfig%5CLev1%5CSASApp%5CStoredProcessServer%5Cautoexec.sas"
,"MF_GETUSER" : "sasdemo"
,"SYSCC" : "0"
,"SYSENCODING" : "wlatin1"
,"SYSERRORTEXT" : ""
,"SYSHOSTNAME" : "SAS"
,"SYSPROCESSID" : "41DD8056A491DB23409E940000000000"
,"SYSPROCESSMODE" : "SAS Stored Process Server"
,"SYSPROCESSNAME" : ""
,"SYSJOBID" : "27448"
,"SYSSCPL" : "X64_DSRV16"
,"SYSSITE" : "123"
,"SYSUSERID" : "sassrv"
,"SYSVLONG" : "9.04.01M7P080520"
,"SYSWARNINGTEXT" : "ENCODING option ignored for files opened with RECFM=N."
,"END_DTTM" : "2022-09-26T08:30:13.853000"
,"MEMSIZE" : "46GB"
}`
_webout = JSON.stringify(data)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -5381,6 +5381,44 @@ let webouts = {
}`
}
/**
* Mirror the editor's demo columns (READONLY/HIDDEN/ROUND/NUMBER_FORMAT) into
* the view payload so the View and Edit screens show the same columns.
*
* The viewer is read-only: it ignores READONLY/ROUND (editor-only), but it DOES
* honour NUMBER_FORMAT (display) and HIDDEN (visibility) via `dqrules` below, so
* the currency column renders and the hidden column is hidden, same as the editor.
* The MPE_X_TEST entry is a JSON string, so we parse, augment
* `cols`/`viewdata`/`dqrules`, then re-stringify.
*/
;(() => {
const v = JSON.parse(webouts.MPE_X_TEST)
v.cols.push(
{ NAME: "READONLY_COL", LENGTH: 200, VARNUM: 11, LABEL: "READONLY_COL", FMTNAME: "", FORMAT: "$200.", TYPE: "C", DDTYPE: "CHARACTER" },
{ NAME: "HIDDEN_COL", LENGTH: 200, VARNUM: 12, LABEL: "HIDDEN_COL", FMTNAME: "", FORMAT: "$200.", TYPE: "C", DDTYPE: "CHARACTER" },
{ NAME: "ROUND_COL", LENGTH: 8, VARNUM: 13, LABEL: "ROUND_COL", FMTNAME: "", FORMAT: "BEST.", TYPE: "N", DDTYPE: "NUMERIC" },
{ NAME: "NUMFMT_COL", LENGTH: 8, VARNUM: 14, LABEL: "NUMFMT_COL", FMTNAME: "", FORMAT: "BEST.", TYPE: "N", DDTYPE: "NUMERIC" }
)
v.viewdata = v.viewdata.map((row, i) => ({
...row,
READONLY_COL: "Readonly default",
HIDDEN_COL: "Hidden default",
ROUND_COL: Number((i + 1 + i / 7).toFixed(5)),
NUMFMT_COL: 1000 + i * 12.5
}))
// NUMBER_FORMAT (display) and HIDDEN (visibility) are honoured by the viewer
// (see viewer.component.ts); READONLY/ROUND are editor-only and omitted here.
v.dqrules = (v.dqrules || []).concat([
{ BASE_COL: "NUMFMT_COL", RULE_TYPE: "NUMBER_FORMAT", RULE_VALUE: '{"style":"currency","currency":"EUR"}', X: 0 },
{ BASE_COL: "HIDDEN_COL", RULE_TYPE: "HIDDEN", RULE_VALUE: "Hidden default", X: 0 }
])
webouts.MPE_X_TEST = JSON.stringify(v)
})()
let table = 'MPE_X_TEST'
if (_WEBIN_FILEREF1) {
+312 -179
View File
@@ -6,8 +6,32 @@
"": {
"name": "dc-sas",
"dependencies": {
"@sasjs/cli": "^4.14.0",
"@sasjs/core": "^4.61.0"
"@sasjs/cli": "4.17.4",
"@sasjs/core": "4.67.1"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
}
},
"node_modules/@asamuzakjp/dom-selector": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-2.0.2.tgz",
"integrity": "sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==",
"license": "MIT",
"dependencies": {
"bidi-js": "^1.0.3",
"css-tree": "^2.3.1",
"is-potential-custom-element-name": "^1.0.1"
}
},
"node_modules/@coolaj86/urequest": {
@@ -16,6 +40,116 @@
"integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA==",
"license": "(MIT OR Apache-2.0)"
},
"node_modules/@csstools/color-helpers": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"engines": {
"node": ">=18"
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^5.1.0",
"@csstools/css-calc": "^2.1.4"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@fast-csv/format": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz",
@@ -66,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",
@@ -80,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",
@@ -115,21 +249,21 @@
}
},
"node_modules/@sasjs/cli": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.14.0.tgz",
"integrity": "sha512-WpZFLxPuh0xBPfX4Vy5kkhvz2QVRLmOwmw70rdHd5DpSw3U4CGY3EbCLVSd0K0CLEBWJLR5EX2gITV8hUcCP4w==",
"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.61.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",
"find": "0.3.0",
"js-base64": "3.7.5",
"jsdom": "22.1.0",
"jsdom": "23.2.0",
"jwt-decode": "3.1.2",
"lodash.groupby": "4.6.0",
"lodash.uniqby": "4.7.0",
@@ -146,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",
@@ -181,9 +315,9 @@
}
},
"node_modules/@sasjs/core": {
"version": "4.61.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.61.0.tgz",
"integrity": "sha512-gpewnAUBDqPOSR1PsRPCB6vba+kY5NL6UyYuSUZFVh1j9Mz5Wkli3eZZjStZABflqKQVQbPNsgIlqw/SpzwGxg==",
"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": {
@@ -233,15 +367,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/@types/fs-extra": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
@@ -276,13 +401,6 @@
"@types/node": "*"
}
},
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
"deprecated": "Use your platform's native atob() and btoa() methods instead",
"license": "BSD-3-Clause"
},
"node_modules/accumulate-stream": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/accumulate-stream/-/accumulate-stream-5.0.0.tgz",
@@ -342,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": {
@@ -407,6 +525,15 @@
],
"license": "MIT"
},
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"license": "MIT",
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -624,30 +751,49 @@
"node": ">= 8"
}
},
"node_modules/cssstyle": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz",
"integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==",
"node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"license": "MIT",
"dependencies": {
"rrweb-cssom": "^0.6.0"
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
},
"engines": {
"node": ">=14"
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/data-urls": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz",
"integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==",
"node_modules/cssstyle": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"license": "MIT",
"dependencies": {
"abab": "^2.0.6",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^12.0.0"
"@asamuzakjp/css-color": "^3.2.0",
"rrweb-cssom": "^0.8.0"
},
"engines": {
"node": ">=14"
"node": ">=18"
}
},
"node_modules/cssstyle/node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"license": "MIT"
},
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/debug": {
@@ -694,19 +840,6 @@
"node": ">=0.4.0"
}
},
"node_modules/domexception": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
"integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
"deprecated": "Use your platform's native DOMException instead",
"license": "MIT",
"dependencies": {
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
@@ -878,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",
@@ -1073,15 +1206,15 @@
}
},
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
"license": "MIT",
"dependencies": {
"whatwg-encoding": "^2.0.0"
"whatwg-encoding": "^3.1.1"
},
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/http-cookie-agent": {
@@ -1109,29 +1242,16 @@
}
},
"node_modules/http-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT",
"dependencies": {
"@tootallnate/once": "2",
"agent-base": "6",
"debug": "4"
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/http-proxy-agent/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
"node": ">= 14"
}
},
"node_modules/https": {
@@ -1141,28 +1261,16 @@
"license": "ISC"
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/https-proxy-agent/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
"node": ">= 14"
}
},
"node_modules/human-signals": {
@@ -1339,40 +1447,38 @@
"license": "BSD-3-Clause"
},
"node_modules/jsdom": {
"version": "22.1.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz",
"integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==",
"version": "23.2.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz",
"integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==",
"license": "MIT",
"dependencies": {
"abab": "^2.0.6",
"cssstyle": "^3.0.0",
"data-urls": "^4.0.0",
"@asamuzakjp/dom-selector": "^2.0.1",
"cssstyle": "^4.0.1",
"data-urls": "^5.0.0",
"decimal.js": "^10.4.3",
"domexception": "^4.0.0",
"form-data": "^4.0.0",
"html-encoding-sniffer": "^3.0.0",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.1",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.4",
"parse5": "^7.1.2",
"rrweb-cssom": "^0.6.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^4.1.2",
"w3c-xmlserializer": "^4.0.0",
"tough-cookie": "^4.1.3",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^12.0.1",
"ws": "^8.13.0",
"xml-name-validator": "^4.0.0"
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0",
"ws": "^8.16.0",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=16"
"node": ">=18"
},
"peerDependencies": {
"canvas": "^2.5.0"
"canvas": "^2.11.2"
},
"peerDependenciesMeta": {
"canvas": {
@@ -1475,6 +1581,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1484,6 +1596,12 @@
"node": ">= 0.4"
}
},
"node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"license": "CC0-1.0"
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -1594,12 +1712,6 @@
"node": ">=8"
}
},
"node_modules/nwsapi": {
"version": "2.2.23",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
"license": "MIT"
},
"node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
@@ -1706,9 +1818,9 @@
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -1731,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",
@@ -1806,6 +1921,15 @@
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -1948,6 +2072,15 @@
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ssl-root-cas": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/ssl-root-cas/-/ssl-root-cas-1.3.1.tgz",
@@ -2036,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",
@@ -2057,15 +2189,15 @@
}
},
"node_modules/tr46": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
"integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.0"
"punycode": "^2.3.1"
},
"engines": {
"node": ">=14"
"node": ">=18"
}
},
"node_modules/traverse-chain": {
@@ -2111,15 +2243,15 @@
"integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA=="
},
"node_modules/w3c-xmlserializer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
"integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"license": "MIT",
"dependencies": {
"xml-name-validator": "^4.0.0"
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=14"
"node": ">=18"
}
},
"node_modules/wcwidth": {
@@ -2141,37 +2273,38 @@
}
},
"node_modules/whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz",
"integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==",
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT",
"dependencies": {
"tr46": "^4.1.1",
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=14"
"node": ">=18"
}
},
"node_modules/which": {
@@ -2207,9 +2340,9 @@
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"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"
@@ -2234,12 +2367,12 @@
"license": "MIT"
},
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"license": "Apache-2.0",
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/xmlchars": {
+2 -2
View File
@@ -28,7 +28,7 @@
},
"private": true,
"dependencies": {
"@sasjs/cli": "^4.14.0",
"@sasjs/core": "^4.61.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';

Some files were not shown because too many files have changed in this diff Show More