Compare commits

..

185 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
semantic-release-bot d8010d4c0c chore(release): 7.4.0 [skip ci]
# [7.4.0](https://git.datacontroller.io/dc/dc/compare/v7.3.0...v7.4.0) (2026-02-20)

### Bug Fixes

* cli bump for mf_getscheme support ([a84ba41](a84ba41ea9))
* missing upcase on SNOW section, plus local sasjs target ([dc20064](dc200646f7))

### Features

* SAS code changes for snowflake support ([e273e87](e273e870ef))
2026-02-20 18:53:31 +00:00
allan a57b49c936 Merge pull request 'feat: SAS code changes for snowflake support' (#208) from sf into main
Release / Build-production-and-ng-test (push) Successful in 4m0s
Release / Build-and-test-development (push) Successful in 9m57s
Release / release (push) Successful in 7m57s
Reviewed-on: #208
2026-02-20 18:36:00 +00:00
allan a84ba41ea9 fix: cli bump for mf_getscheme support
Build / Build-and-ng-test (pull_request) Successful in 4m10s
Build / Build-and-test-development (pull_request) Successful in 10m4s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m58s
2026-02-20 18:15:31 +00:00
allan dc200646f7 fix: missing upcase on SNOW section, plus local sasjs target
Build / Build-and-ng-test (pull_request) Successful in 4m11s
Build / Build-and-test-development (pull_request) Successful in 9m55s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m25s
2026-02-20 01:08:55 +00:00
allan e273e870ef feat: SAS code changes for snowflake support
Build / Build-and-ng-test (pull_request) Successful in 4m9s
Build / Build-and-test-development (pull_request) Successful in 10m1s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m57s
2026-02-20 00:15:23 +00:00
semantic-release-bot 6fc34aca00 chore(release): 7.3.0 [skip ci]
# [7.3.0](https://git.datacontroller.io/dc/dc/compare/v7.2.8...v7.3.0) (2026-02-10)

### Bug Fixes

* bump xlsx, add crypto-shim ([8dc18b1](8dc18b155a))
* correctly applying deletes on viya, also ([46cdeb0](46cdeb0bab))
* crypto module requirement for sheetjs/crypto package ([505d0af](505d0af2b3))
* disable parsing excel in web worker beacuse it breaks in the stream apps ([280bdee](280bdeeb1b))
* Display all contexts when installing DC on Viya ([d41f88f](d41f88f8bf))
* **edit:** use cellValidation keys and hotDataSchema to fill in defaults on add row ([4957548](495754816c))
* enabling closeouts for UPDATE in CAS tables ([8b8e8ae](8b8e8aec15))
* enabling rollback when the table has formatted values ([815d6e9](815d6e97a8))
* improvements to validations ([6ceb681](6ceb681463))
* remove IE checks and conditions ([ece6bd1](ece6bd1d78))
* updates to demodata to enable auto CAS promote ([7740d2a](7740d2ac86))
* upgrade angular core and compiler ([aecd597](aecd597687))
* using fcopy instead of binary copy for file upload, for Viya 2026 compatibility ([716ee6e](716ee6eba0))
* **viewer:** search causing blank Handsontable ([338c7a2](338c7a2e41)), closes [#206](#206)

### Features

* adding demo data job ([8c2aeac](8c2aeacc85))
* **dq rules:** notnull validation when invalid cell, will auto populate a default value ([96f2518](96f2518af9))
2026-02-10 19:17:29 +00:00
allan f97ac70678 Merge pull request 'demodata' (#203) from demodata into main
Release / Build-production-and-ng-test (push) Successful in 3m27s
Release / Build-and-test-development (push) Successful in 9m22s
Release / release (push) Successful in 7m53s
Reviewed-on: #203
2026-02-10 19:01:17 +00:00
allan 6ceb681463 fix: improvements to validations
Build / Build-and-ng-test (pull_request) Successful in 3m47s
Build / Build-and-test-development (pull_request) Successful in 9m38s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m21s
2026-02-10 18:26:02 +00:00
allan 716ee6eba0 fix: using fcopy instead of binary copy for file upload, for Viya 2026 compatibility
Build / Build-and-ng-test (pull_request) Successful in 3m42s
Build / Build-and-test-development (pull_request) Successful in 9m33s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m44s
2026-02-10 16:34:09 +00:00
allan f6b0f6b0cd Merge pull request 'fix(viewer): search causing blank Handsontable' (#207) from fix/206-search-issue into demodata
Build / Build-and-ng-test (pull_request) Successful in 3m39s
Build / Build-and-test-development (pull_request) Successful in 9m37s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m31s
Reviewed-on: #207
2026-02-10 15:50:42 +00:00
sead 737a652ff0 refactor(viewer): use drop instead of debounce
Build / Build-and-ng-test (pull_request) Successful in 3m40s
Build / Build-and-test-development (pull_request) Successful in 9m31s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m32s
2026-02-10 15:36:20 +01:00
sead 2995e5c9dc chore: restore comments and condition
Build / Build-and-ng-test (pull_request) Successful in 3m52s
Build / Build-and-test-development (pull_request) Successful in 9m39s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m8s
2026-02-10 14:37:56 +01:00
sead 338c7a2e41 fix(viewer): search causing blank Handsontable
Build / Build-and-ng-test (pull_request) Successful in 3m46s
Build / Build-and-test-development (pull_request) Successful in 9m26s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m52s
Closes #206
2026-02-10 14:24:06 +01:00
allan ad27358deb Merge pull request 'fix(edit): use cellValidation keys and hotDataSchema to fill in defaults on add row' (#205) from fix/204-default-value into demodata
Build / Build-and-ng-test (pull_request) Successful in 4m15s
Build / Build-and-test-development (pull_request) Successful in 10m18s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m50s
Reviewed-on: #205
2026-02-10 12:58:15 +00:00
sead 495754816c fix(edit): use cellValidation keys and hotDataSchema to fill in defaults on add row
Build / Build-and-ng-test (pull_request) Successful in 3m40s
Build / Build-and-test-development (pull_request) Successful in 10m33s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 26m51s
2026-02-10 13:53:29 +01:00
M 96f2518af9 feat(dq rules): notnull validation when invalid cell, will auto populate a default value
Build / Build-and-ng-test (pull_request) Successful in 3m41s
Build / Build-and-test-development (pull_request) Successful in 9m30s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m50s
2026-02-10 12:29:15 +01:00
M 280bdeeb1b fix: disable parsing excel in web worker beacuse it breaks in the stream apps
Build / Build-and-ng-test (pull_request) Successful in 3m41s
Build / Build-and-test-development (pull_request) Successful in 9m41s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m0s
2026-02-10 11:58:00 +01:00
allan 46cdeb0bab fix: correctly applying deletes on viya, also
Build / Build-and-ng-test (pull_request) Successful in 3m32s
Build / Build-and-test-development (pull_request) Successful in 9m14s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m18s
adding more info to the staged directory in relation to deletes.  Also
refactoring the demo data.
2026-02-09 23:23:08 +00:00
allan d41f88f8bf fix: Display all contexts when installing DC on Viya
Build / Build-and-ng-test (pull_request) Successful in 3m58s
Build / Build-and-test-development (pull_request) Successful in 9m48s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m29s
2026-02-09 18:22:06 +00:00
allan 815d6e97a8 fix: enabling rollback when the table has formatted values
Build / Build-and-ng-test (pull_request) Successful in 3m35s
Build / Build-and-test-development (pull_request) Successful in 9m16s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m4s
2026-02-08 02:40:07 +00:00
allan 4e35aefe41 Merge pull request 'Upgrade angular core and compiler' (#199) from fix/audit-20260112 into demodata
Build / Build-and-ng-test (pull_request) Successful in 3m59s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m16s
Build / Build-and-test-development (pull_request) Successful in 9m48s
Reviewed-on: #199
Reviewed-by: mihajlo <mihajlo@4gl.io>
2026-02-07 23:46:19 +00:00
allan ca84915e43 Merge branch 'demodata' into fix/audit-20260112
Build / Build-and-ng-test (pull_request) Successful in 4m15s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m43s
Build / Build-and-test-development (pull_request) Successful in 9m56s
2026-02-07 23:46:10 +00:00
zver 31cc7e9e4d chore: server release and more demodata
Build / Build-and-ng-test (pull_request) Successful in 4m3s
Build / Build-and-test-development (pull_request) Successful in 9m46s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m52s
2026-02-07 23:44:37 +00:00
allan 4ec107705e Merge branch 'main' into demodata
Build / Build-and-ng-test (pull_request) Successful in 3m57s
Build / Build-and-test-development (pull_request) Successful in 9m9s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Failing after 3h13m38s
2026-02-07 20:04:16 +00:00
zver 7740d2ac86 fix: updates to demodata to enable auto CAS promote
Build / Build-and-ng-test (pull_request) Successful in 3m36s
Build / Build-and-test-development (pull_request) Successful in 9m2s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Failing after 3h13m50s
2026-02-07 20:02:14 +00:00
zver 8c2aeacc85 feat: adding demo data job 2026-02-06 20:50:55 +00:00
zver 8b8e8aec15 fix: enabling closeouts for UPDATE in CAS tables 2026-02-06 20:50:38 +00:00
semantic-release-bot 6ac3f660e9 chore(release): 7.2.8 [skip ci]
## [7.2.8](https://git.datacontroller.io/dc/dc/compare/v7.2.7...v7.2.8) (2026-02-06)

### Bug Fixes

* bump adapter version ([f4c8699](f4c8699aaf))
2026-02-06 12:51:50 +00:00
sead 7ee576a9c1 Merge pull request 'Update @sasjs/adapter version' (#202) from fix/bump-adapter-20260206 into main
Release / Build-production-and-ng-test (push) Successful in 3m32s
Release / Build-and-test-development (push) Successful in 9m7s
Release / release (push) Successful in 7m39s
Reviewed-on: #202
2026-02-06 12:35:55 +00:00
sead f4c8699aaf fix: bump adapter version
Build / Build-and-ng-test (pull_request) Successful in 3m48s
Build / Build-and-test-development (pull_request) Successful in 9m12s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m14s
2026-02-06 13:12:51 +01:00
zver 4273ca6e5c chore: demo data job 2026-02-06 02:13:23 +00:00
semantic-release-bot 4ba043b77e chore(release): 7.2.7 [skip ci]
## [7.2.7](https://git.datacontroller.io/dc/dc/compare/v7.2.6...v7.2.7) (2026-02-05)

### Bug Fixes

* dclib not found error in getchangeinfo job ([86791db](86791dbaca))
2026-02-05 19:15:36 +00:00
allan 0169415ea2 Merge pull request 'fix: dclib not found error in getchangeinfo job' (#201) from dclibfix into main
Release / Build-production-and-ng-test (push) Successful in 3m32s
Release / Build-and-test-development (push) Successful in 9m15s
Release / release (push) Successful in 7m56s
Reviewed-on: #201
2026-02-05 18:59:19 +00:00
allan 86791dbaca fix: dclib not found error in getchangeinfo job
Build / Build-and-ng-test (pull_request) Successful in 3m48s
Build / Build-and-test-development (pull_request) Successful in 9m21s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m34s
2026-02-05 16:53:23 +00:00
sead d5b58a3cbd test(excel): password tests - click away overlay modal
Build / Build-and-ng-test (pull_request) Successful in 3m39s
Build / Build-and-test-development (pull_request) Successful in 9m28s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 17m38s
2026-01-14 17:38:36 +01:00
sead 3d8281d27e test(excel): fix attachFile upload for password protected tests
Build / Build-and-ng-test (pull_request) Successful in 3m43s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m3s
Build / Build-and-test-development (pull_request) Failing after 11m3s
2026-01-14 15:47:10 +01:00
sead b1a014c7bc test(excel): add password protected excels tests
Build / Build-and-ng-test (pull_request) Successful in 3m36s
Build / Build-and-test-development (pull_request) Failing after 10m17s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m22s
2026-01-13 18:50:17 +01:00
sead 505d0af2b3 fix: crypto module requirement for sheetjs/crypto package
Undid the crypto-shim changes from 8dc18b155a
2026-01-13 18:41:56 +01:00
sead ece6bd1d78 fix: remove IE checks and conditions 2026-01-13 17:00:56 +01:00
sead 8dc18b155a fix: bump xlsx, add crypto-shim
Build / Build-and-ng-test (pull_request) Successful in 3m29s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m24s
Build / Build-and-test-development (pull_request) Successful in 9m33s
crypto-shim fixes vulnerable crypto-browserify package used by sheetjs/crypto, shim is based on crypto-js
2026-01-13 15:04:17 +01:00
sead aecd597687 fix: upgrade angular core and compiler
Build / Build-and-ng-test (pull_request) Successful in 3m50s
Build / Build-and-test-development (pull_request) Successful in 9m8s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m27s
2026-01-13 08:38:29 +01:00
allan c99f106bae Merge pull request 'Fix lighthouse ci save path' (#198) from fix/lighthouse-20251230 into main
Release / Build-production-and-ng-test (push) Successful in 3m31s
Release / Build-and-test-development (push) Successful in 9m14s
Release / release (push) Failing after 3m16s
Reviewed-on: #198
Reviewed-by: mihajlo <mihajlo@4gl.io>
2026-01-08 11:38:32 +00:00
allan d2fc7ae6fe Merge branch 'main' into fix/lighthouse-20251230
Build / Build-and-ng-test (pull_request) Successful in 4m15s
Build / Build-and-test-development (pull_request) Successful in 9m42s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m16s
2026-01-08 11:38:25 +00:00
semantic-release-bot c1c1d0055a chore(release): 7.2.6 [skip ci]
## [7.2.6](https://git.datacontroller.io/dc/dc/compare/v7.2.5...v7.2.6) (2026-01-05)

### Bug Fixes

* **deps:** update angular and moment ([8c5b357](8c5b357dd2))
2026-01-05 13:56:25 +00:00
sead f37ec82d39 Merge branch 'main' into fix/lighthouse-20251230
Build / Build-and-ng-test (pull_request) Successful in 4m4s
Build / Build-and-test-development (pull_request) Successful in 10m9s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m4s
2026-01-05 13:39:20 +00:00
sead 6e9e30e0f0 Merge pull request 'npm audit vulnerabilities' (#197) from fix/audit-20251229 into main
Release / Build-production-and-ng-test (push) Successful in 3m54s
Release / Build-and-test-development (push) Successful in 10m5s
Release / release (push) Successful in 7m57s
Reviewed-on: #197
Reviewed-by: mihajlo <mihajlo@4gl.io>
2026-01-05 13:39:02 +00:00
sead 990ddb5cd3 chore: update .gitignore
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Has been cancelled
Build / Build-and-test-development (pull_request) Has been cancelled
Build / Build-and-ng-test (pull_request) Has been cancelled
2025-12-30 14:55:33 +01:00
sead c6ebbb48bb ci: fix lighthouse artifacts save path
Build / Build-and-ng-test (pull_request) Successful in 3m55s
Build / Build-and-test-development (pull_request) Successful in 8m9s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m30s
2025-12-30 14:49:01 +01:00
sead 95cc0b1c91 chore: undo unnecessary changes
Build / Build-and-ng-test (pull_request) Successful in 3m49s
Build / Build-and-test-development (pull_request) Successful in 9m19s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m34s
2025-12-30 13:42:20 +01:00
sead 81b282f1f1 chore: revert from esbuild back to webpack
Build / Build-and-ng-test (pull_request) Successful in 3m41s
Build / Build-and-test-development (pull_request) Successful in 9m3s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m31s
2025-12-29 23:14:51 +01:00
sead 8c5b357dd2 fix(deps): update angular and moment
Build / Build-and-ng-test (pull_request) Successful in 2m28s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Has been cancelled
Build / Build-and-test-development (pull_request) Has been cancelled
2025-12-29 22:15:47 +01:00
sead a13b2cbfd2 chore: angular update from v18 to v19 2025-12-29 18:23:21 +01:00
sead 19617c2285 chore: ng update cdk 2025-12-29 17:01:48 +01:00
sead f9794a973f chore: ng update cli, schematics, and core 2025-12-29 16:53:33 +01:00
sead 65efe62d19 chore: prettier 2025-12-29 16:46:40 +01:00
sead 683ddcaf53 chore: add ng command 2025-12-29 16:46:11 +01:00
semantic-release-bot 113e0bbc3c chore(release): 7.2.5 [skip ci]
## [7.2.5](https://git.datacontroller.io/dc/dc/compare/v7.2.4...v7.2.5) (2025-12-09)

### Bug Fixes

* (build) rebuilt package-lock files ([bfbfd55](bfbfd55fe7))
* (deps) bump @sasjs/cli and @sasjs/core ([d7c7302](d7c7302c12))
* (deps) bumped @sasjs/core, @sasjs/cli, @sasjs/utils and @sasjs/adapter ([af1657e](af1657e226))
* configurable audit table on restore check ([26ce95f](26ce95f7c1)), closes [#193](#193)
* improved testing ([fb3c49a](fb3c49aa8b))
* output values to intended macro variables ([43ae73c](43ae73c5f3))
2025-12-09 12:27:01 +00:00
allan 2af97e40bf Merge pull request 'fix: (build) bump node version for pipelines' (#196) from node_version_bump_for_pipelines into main
Release / Build-production-and-ng-test (push) Successful in 3m23s
Release / Build-and-test-development (push) Successful in 7m59s
Release / release (push) Successful in 7m3s
Reviewed-on: #196
2025-12-09 12:12:31 +00:00
Trevor Moody 83cbe3aece chore: (build) bump node version for pipelines
Build / Build-and-ng-test (pull_request) Successful in 3m36s
Build / Build-and-test-development (pull_request) Successful in 8m10s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m11s
2025-12-09 11:49:56 +00:00
allan ceac1ba614 Merge pull request 'bumpCoreAndCli_20251125' (#195) from bumpCoreAndCli_20251125 into main
Release / Build-production-and-ng-test (push) Successful in 3m39s
Release / Build-and-test-development (push) Successful in 8m11s
Release / release (push) Failing after 3m19s
Reviewed-on: #195
2025-12-08 19:02:14 +00:00
allan 765fdbdf9d chore: merge
Build / Build-and-ng-test (pull_request) Successful in 3m33s
Build / Build-and-test-development (pull_request) Successful in 8m16s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 18m46s
2025-12-08 17:41:07 +00:00
Trevor Moody 43ae73c5f3 fix: output values to intended macro variables
Build / Build-and-ng-test (pull_request) Successful in 3m33s
Build / Build-and-test-development (pull_request) Successful in 8m19s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 19m13s
2025-12-08 16:55:22 +00:00
Trevor Moody e57a0de8a9 chore: Re-basing file due to merge conflict 2025-12-08 16:53:03 +00:00
Trevor Moody 3de491105b chore: (build) amended licenseChecker.js exclusions
Build / Build-and-ng-test (pull_request) Successful in 3m35s
Build / Build-and-test-development (pull_request) Successful in 8m11s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 19m7s
2025-12-08 16:04:41 +00:00
M 2fe690e962 chore: package-lock
Build / Build-and-ng-test (pull_request) Failing after 1m27s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 1m46s
Build / Build-and-test-development (pull_request) Successful in 8m6s
2025-12-08 15:49:46 +01:00
M b826d37086 chore: package-lock
Build / Build-and-ng-test (pull_request) Failing after 39s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 53s
Build / Build-and-test-development (pull_request) Failing after 42s
2025-12-08 15:42:03 +01:00
Trevor Moody bfbfd55fe7 fix: (build) rebuilt package-lock files
Build / Build-and-ng-test (pull_request) Failing after 38s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 58s
Build / Build-and-test-development (pull_request) Failing after 44s
2025-12-08 14:16:56 +00:00
Trevor Moody 15f38efd52 chore: (build) use node 24.5.0
Build / Build-and-ng-test (pull_request) Failing after 45s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 56s
Build / Build-and-test-development (pull_request) Failing after 41s
2025-12-08 13:02:50 +00:00
Trevor Moody 5d25681485 chore: lint fixes
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 57s
Build / Build-and-test-development (pull_request) Failing after 42s
Build / Build-and-ng-test (pull_request) Failing after 41s
2025-12-08 12:30:00 +00:00
Trevor Moody d26df376f8 chore: CRLF to LF
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 56s
Build / Build-and-test-development (pull_request) Failing after 41s
Build / Build-and-ng-test (pull_request) Failing after 37s
2025-12-08 11:41:12 +00:00
allan cff3fb3bad Merge pull request 'fix: configurable audit table on restore check' (#194) from issue193 into main
Release / Build-production-and-ng-test (push) Successful in 3m36s
Release / Build-and-test-development (push) Successful in 8m15s
Release / release (push) Failing after 3m20s
Reviewed-on: #194
2025-12-08 11:25:44 +00:00
Trevor Moody fb3c49aa8b fix: improved testing
Build / Build-and-ng-test (pull_request) Failing after 39s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 57s
Build / Build-and-test-development (pull_request) Failing after 58s
2025-12-08 10:21:37 +00:00
Trevor Moody af1657e226 fix: (deps) bumped @sasjs/core, @sasjs/cli, @sasjs/utils and @sasjs/adapter 2025-11-28 04:11:46 +00:00
Trevor Moody d7c7302c12 fix: (deps) bump @sasjs/cli and @sasjs/core 2025-11-25 14:24:36 +00:00
allan 26ce95f7c1 fix: configurable audit table on restore check
Build / Build-and-ng-test (pull_request) Successful in 5m41s
Build / Build-and-test-development (pull_request) Successful in 9m29s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 19m20s
closes #193
2025-10-27 20:13:49 +00:00
semantic-release-bot 4924df2ef3 chore(release): 7.2.4 [skip ci]
## [7.2.4](https://git.datacontroller.io/dc/dc/compare/v7.2.3...v7.2.4) (2025-10-14)

### Bug Fixes

* ensure reload after applying licence key ([cb1978b](cb1978bcaf))
* snyk report security patches ([387f512](387f5122f1))
2025-10-14 11:44:18 +00:00
sead 2e141a5d52 chore(git): merge pull request 'fix: snyk report security patches' (#192) from dc_snyk into main
Release / Build-production-and-ng-test (push) Successful in 3m42s
Release / Build-and-test-development (push) Successful in 8m52s
Release / release (push) Successful in 8m13s
Reviewed-on: #192
Reviewed-by: mihajlo <mihajlo@4gl.io>
Reviewed-by: sead <sead@noreply.git.datacontroller.io>
2025-10-14 11:28:05 +00:00
Trevor Moody cb1978bcaf fix: ensure reload after applying licence key
Build / Build-and-ng-test (pull_request) Successful in 4m2s
Build / Build-and-test-development (pull_request) Successful in 8m49s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 18m48s
2025-10-14 12:05:27 +01:00
Trevor Moody 387f5122f1 fix: snyk report security patches
Build / Build-and-ng-test (pull_request) Successful in 4m0s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 19m23s
Build / Build-and-test-development (pull_request) Failing after 11m1s
2025-10-10 12:54:22 +01:00
semantic-release-bot db5887de21 chore(release): 7.2.3 [skip ci]
## [7.2.3](https://git.datacontroller.io/dc/dc/compare/v7.2.2...v7.2.3) (2025-10-02)

### Bug Fixes

* opening second table in viewer throws an error ([6c6b1cb](6c6b1cbf46))
2025-10-02 15:10:30 +00:00
allan fe24d9bcbd Merge pull request 'opening second table in viewer throws an error' (#191) from issue-189 into main
Release / Build-production-and-ng-test (push) Successful in 3m47s
Release / Build-and-test-development (push) Successful in 8m52s
Release / release (push) Successful in 8m25s
Reviewed-on: #191
Reviewed-by: allan <allan@4gl.io>
2025-10-02 14:54:13 +00:00
M 6c6b1cbf46 fix: opening second table in viewer throws an error
Build / Build-and-ng-test (pull_request) Successful in 4m59s
Build / Build-and-test-development (pull_request) Successful in 9m5s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 19m55s
2025-10-02 16:23:00 +02:00
207 changed files with 19852 additions and 17954 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+49 -37
View File
@@ -2,39 +2,53 @@ name: Build
run-name: Running Lint Check and Licence checker on Pull Request run-name: Running Lint Check and Licence checker on Pull Request
on: [pull_request] on: [pull_request]
env:
NODE_VERSION: '24.15.0'
jobs: jobs:
Build-and-ng-test: Build-and-ng-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.15.1 node-version: ${{ env.NODE_VERSION }}
- name: Install Google Chrome - name: Install Google Chrome
run: | 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 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 - name: Write .npmrc file
run: echo "$NPMRC" > client/.npmrc run: echo "$NPMRC" >> client/.npmrc
shell: bash shell: bash
env: env:
NPMRC: ${{ secrets.NPMRC}} NPMRC: ${{ secrets.NPMRC}}
- name: Lint check
run: npm run lint:check
- name: Install dependencies - name: Install dependencies
run: | run: |
cd client cd client
# Decrypt and Install sheet # 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 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 - name: Licence checker
run: | run: |
cd client cd client
@@ -52,26 +66,27 @@ jobs:
Build-and-test-development: Build-and-test-development:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: Build-production-and-ng-test needs: Build-and-ng-test
env:
CHROME_BIN: /usr/bin/google-chrome
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.15.1 node-version: ${{ env.NODE_VERSION }}
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
touch client/.npmrc touch client/.npmrc
echo '${{ secrets.NPMRC}}' > client/.npmrc echo '${{ secrets.NPMRC}}' > client/.npmrc
- run: apt-get update - name: Install system dependencies
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb run: |
- run: apt install -y ./google-chrome*.deb; apt-get update
- run: export CHROME_BIN=/usr/bin/google-chrome wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- run: apt-get update -y apt install -y ./google-chrome*.deb
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb jq zip
- run: apt -y install jq
- name: Write cypress credentials - name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json 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 echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
npm ci npm ci
# Install pm2 and prepare SASJS server - name: Setup and start SASjs server
- run: npm i -g pm2 run: |
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip npm i -g pm2
- run: unzip linux.zip curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- run: touch .env unzip linux.zip
- run: echo RUN_TIMES=js >> .env touch .env
- run: echo NODE_PATH=node >> .env echo RUN_TIMES=js >> .env
- run: echo CORS=enable >> .env echo NODE_PATH=node >> .env
- run: echo WHITELIST=http://localhost:4200 >> .env echo CORS=enable >> .env
- run: cat .env echo WHITELIST=http://localhost:4200 >> .env
- run: pm2 start api-linux --wait-ready cat .env
pm2 start api-linux --wait-ready
- name: Deploy mocked services - name: Deploy mocked services
run: | run: |
@@ -106,11 +122,6 @@ jobs:
sasjs cbd -t server-ci sasjs cbd -t server-ci
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json # 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 - name: Prepare and run frontend and cypress
run: | run: |
cd ./client cd ./client
@@ -126,11 +137,12 @@ jobs:
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
cat ./cypress.config.ts cat ./cypress.config.ts
# Start frontend and run cypress # 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 - name: Zip Cypress videos
if: always() if: always()
run: | run: |
mkdir -p ./client/cypress/videos
zip -r cypress-videos ./client/cypress/videos zip -r cypress-videos ./client/cypress/videos
- name: Add cypress videos artifacts - name: Add cypress videos artifacts
+19 -29
View File
@@ -2,38 +2,31 @@ name: Lighthouse Checks
run-name: Running Lighthouse Performance and Accessibility Checks on Pull Request run-name: Running Lighthouse Performance and Accessibility Checks on Pull Request
on: [pull_request] on: [pull_request]
env:
NODE_VERSION: '24.15.0'
jobs: jobs:
lighthouse: lighthouse:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.15.1]
steps: 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 uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ env.NODE_VERSION }}
- name: Install Google Chrome - name: Install Google Chrome
run: | 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 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 - name: Install global packages
run: npm i -g pm2 run: npm i -g pm2 @sasjs/cli wait-on
- name: Install @sasjs/cli - name: Setup and start SASjs server
run: npm i -g @sasjs/cli
- name: Install wait-on globally
run: npm install -g wait-on
- name: Create .env file for sasjs/server
run: | run: |
touch .env touch .env
echo RUN_TIMES=js >> .env echo RUN_TIMES=js >> .env
@@ -41,15 +34,9 @@ jobs:
echo CORS=enable >> .env echo CORS=enable >> .env
echo WHITELIST=http://localhost:4200 >> .env echo WHITELIST=http://localhost:4200 >> .env
cat .env cat .env
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- name: Download sasjs/server package from github using curl unzip linux.zip
run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip pm2 start api-linux --wait-ready
- name: Unzip downloaded package
run: unzip linux.zip
- name: Run sasjs server
run: pm2 start api-linux --wait-ready
- name: Write .npmrc file - name: Write .npmrc file
run: echo "$NPMRC" > client/.npmrc run: echo "$NPMRC" > client/.npmrc
@@ -61,7 +48,10 @@ jobs:
run: | run: |
cd client cd client
# Decrypt and Install sheet # 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 ci
npm install -g replace-in-files-cli npm install -g replace-in-files-cli
@@ -98,4 +88,4 @@ jobs:
with: with:
name: Lighthouse results name: Lighthouse results
path: client/lighthouse-reports path: client/lighthouse-reports
include-hidden-files: true include-hidden-files: true
+45 -47
View File
@@ -5,27 +5,31 @@ on:
branches: branches:
- main - main
env:
NODE_VERSION: '24.5.0'
jobs: jobs:
Build-production-and-ng-test: Build-production-and-ng-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CHROME_BIN: /usr/bin/google-chrome
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.14.0 node-version: ${{ env.NODE_VERSION }}
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
touch client/.npmrc touch client/.npmrc
echo '${{ secrets.NPMRC}}' > client/.npmrc echo '${{ secrets.NPMRC}}' > client/.npmrc
- name: Install Chrome for Angular tests - name: Install Chrome for Angular tests
run: | run: |
apt-get update apt-get update
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome*.deb; apt install -y ./google-chrome*.deb
export CHROME_BIN=/usr/bin/google-chrome
- name: Write cypress credentials - name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
@@ -41,11 +45,11 @@ jobs:
npm ci npm ci
- name: Check audit - name: Check audit
# Audit should fail and stop the CI if critical vulnerability found # Audit should fail and stop the CI if critical vulnerability found
run: | run: |
npm audit --audit-level=critical --omit=dev npm audit --omit=dev
cd ./sas cd ./sas
npm audit --audit-level=critical --omit=dev npm audit --omit=dev
cd ../client cd ../client
npm audit --audit-level=critical --omit=dev npm audit --audit-level=critical --omit=dev
@@ -63,25 +67,26 @@ jobs:
Build-and-test-development: Build-and-test-development:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: Build-production-and-ng-test needs: Build-production-and-ng-test
env:
CHROME_BIN: /usr/bin/google-chrome
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.14.0 node-version: ${{ env.NODE_VERSION }}
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
touch client/.npmrc touch client/.npmrc
echo '${{ secrets.NPMRC}}' > client/.npmrc echo '${{ secrets.NPMRC}}' > client/.npmrc
- run: apt-get update - name: Install system dependencies
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb run: |
- run: apt install -y ./google-chrome*.deb; apt-get update
- run: export CHROME_BIN=/usr/bin/google-chrome wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- run: apt-get update -y apt install -y ./google-chrome*.deb
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb jq zip
- run: apt -y install jq
- name: Write cypress credentials - name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json 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 echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
npm ci npm ci
# Install pm2 and prepare SASJS server - name: Setup and start SASjs server
- run: npm i -g pm2 run: |
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip npm i -g pm2
- run: unzip linux.zip curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- run: touch .env unzip linux.zip
- run: echo RUN_TIMES=js >> .env touch .env
- run: echo NODE_PATH=node >> .env echo RUN_TIMES=js >> .env
- run: echo CORS=enable >> .env echo NODE_PATH=node >> .env
- run: echo WHITELIST=http://localhost:4200 >> .env echo CORS=enable >> .env
- run: cat .env echo WHITELIST=http://localhost:4200 >> .env
- run: pm2 start api-linux --wait-ready cat .env
pm2 start api-linux --wait-ready
- name: Deploy mocked services - name: Deploy mocked services
run: | run: |
@@ -116,11 +122,6 @@ jobs:
sasjs cbd -t server-ci sasjs cbd -t server-ci
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json # 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 - name: Prepare and run frontend and cypress
run: | run: |
cd ./client cd ./client
@@ -136,11 +137,12 @@ jobs:
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
cat ./cypress.config.ts cat ./cypress.config.ts
# Start frontend and run cypress # 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 - name: Zip Cypress videos
if: always() if: always()
run: | run: |
mkdir -p ./client/cypress/videos
zip -r cypress-videos ./client/cypress/videos zip -r cypress-videos ./client/cypress/videos
- name: Add cypress videos artifacts - name: Add cypress videos artifacts
@@ -155,10 +157,10 @@ jobs:
needs: [Build-production-and-ng-test, Build-and-test-development] needs: [Build-production-and-ng-test, Build-and-test-development]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.14.0 node-version: ${{ env.NODE_VERSION }}
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
@@ -168,17 +170,11 @@ jobs:
env: env:
NPMRC: ${{ secrets.NPMRC}} NPMRC: ${{ secrets.NPMRC}}
- name: Install packages - name: Install system packages
run: | run: |
apt-get update apt-get update
apt-get install zip -y apt-get install -y zip jq doxygen
# sasjs cli is used to compile & build the SAS services
npm i -g @sasjs/cli 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 - name: Frontend Preliminary Build
description: We want to prevent creating empty release if frontend fails description: We want to prevent creating empty release if frontend fails
@@ -228,6 +224,8 @@ jobs:
cp sasjs/utils/favicon.ico ../client/dist/favicon.ico cp sasjs/utils/favicon.ico ../client/dist/favicon.ico
sasjs c -t server sasjs c -t server
rm -rf sasjsbuild/tests rm -rf sasjsbuild/tests
server_apploc="/Public/app/dc"
sed -i "s|apploc=\"[^\"]*\"|apploc=\"${server_apploc}\"|g" sasjsbuild/services/web/index.html
sasjs b -t server sasjs b -t server
cp sasjsbuild/server.json.zip ./sasjs_server.json.zip cp sasjsbuild/server.json.zip ./sasjs_server.json.zip
+1
View File
@@ -14,6 +14,7 @@ client/documentation
client/**/sheet-crypto.tgz client/**/sheet-crypto.tgz
client/.nx client/.nx
client/libraries/sheet-crypto.tgz client/libraries/sheet-crypto.tgz
client/lighthouse-reports
cypress.env.json cypress.env.json
sasjsbuild sasjsbuild
sasjsresults sasjsresults
+3
View File
@@ -1 +1,4 @@
legacy-peer-deps=true legacy-peer-deps=true
ignore-scripts=true
save-exact=true
fund=false
+199
View File
@@ -1,3 +1,202 @@
## [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)
### Bug Fixes
* cli bump for mf_getscheme support ([a84ba41](https://git.datacontroller.io/dc/dc/commit/a84ba41ea9f0c97ae24f0a572b8cf5ec200f2132))
* missing upcase on SNOW section, plus local sasjs target ([dc20064](https://git.datacontroller.io/dc/dc/commit/dc200646f7df2fd1910841f392c314532aae7581))
### Features
* SAS code changes for snowflake support ([e273e87](https://git.datacontroller.io/dc/dc/commit/e273e870efbf7875db869b760f2c7b1f39d571ae))
# [7.3.0](https://git.datacontroller.io/dc/dc/compare/v7.2.8...v7.3.0) (2026-02-10)
### Bug Fixes
* bump xlsx, add crypto-shim ([8dc18b1](https://git.datacontroller.io/dc/dc/commit/8dc18b155abfc20fd0b043e0d70bbbc17e6b6811))
* correctly applying deletes on viya, also ([46cdeb0](https://git.datacontroller.io/dc/dc/commit/46cdeb0babee6870553a41877cbe85204e7099c4))
* crypto module requirement for sheetjs/crypto package ([505d0af](https://git.datacontroller.io/dc/dc/commit/505d0af2b3b3c1c79c65045dcaffc263e0f8e796))
* disable parsing excel in web worker beacuse it breaks in the stream apps ([280bdee](https://git.datacontroller.io/dc/dc/commit/280bdeeb1b82f00689f46c68a3cde3f2d24bc18f))
* Display all contexts when installing DC on Viya ([d41f88f](https://git.datacontroller.io/dc/dc/commit/d41f88f8bf5bb2c725ee3edba085e8961ab8c727))
* **edit:** use cellValidation keys and hotDataSchema to fill in defaults on add row ([4957548](https://git.datacontroller.io/dc/dc/commit/495754816c0e757b8f8b1c0ad51246dc7b65d957))
* enabling closeouts for UPDATE in CAS tables ([8b8e8ae](https://git.datacontroller.io/dc/dc/commit/8b8e8aec159ff2f50cfa4683bcd7a25aabb75bf8))
* enabling rollback when the table has formatted values ([815d6e9](https://git.datacontroller.io/dc/dc/commit/815d6e97a8e304d79d48cc949ba126e02a318dc1))
* improvements to validations ([6ceb681](https://git.datacontroller.io/dc/dc/commit/6ceb6814633691b6d4ac2cb898cfb75e9d609102))
* remove IE checks and conditions ([ece6bd1](https://git.datacontroller.io/dc/dc/commit/ece6bd1d787d722531334fc4f1396a94cf6d92ec))
* updates to demodata to enable auto CAS promote ([7740d2a](https://git.datacontroller.io/dc/dc/commit/7740d2ac8694295b33b40a30603d8239818896f5))
* upgrade angular core and compiler ([aecd597](https://git.datacontroller.io/dc/dc/commit/aecd5976875a7c01189248c5f5aa3478b28c1ab2))
* using fcopy instead of binary copy for file upload, for Viya 2026 compatibility ([716ee6e](https://git.datacontroller.io/dc/dc/commit/716ee6eba0a28f4f0a7a96b0719caf15da9b6e78))
* **viewer:** search causing blank Handsontable ([338c7a2](https://git.datacontroller.io/dc/dc/commit/338c7a2e418c47e34331bd04718cd816f978837c)), closes [#206](https://git.datacontroller.io/dc/dc/issues/206)
### Features
* adding demo data job ([8c2aeac](https://git.datacontroller.io/dc/dc/commit/8c2aeacc85da5c106c356709cefcb412ed0a71db))
* **dq rules:** notnull validation when invalid cell, will auto populate a default value ([96f2518](https://git.datacontroller.io/dc/dc/commit/96f2518af9e547956be5862a1322d9ab8e07369b))
## [7.2.8](https://git.datacontroller.io/dc/dc/compare/v7.2.7...v7.2.8) (2026-02-06)
### Bug Fixes
* bump adapter version ([f4c8699](https://git.datacontroller.io/dc/dc/commit/f4c8699aaf0b1e01b447296978a4f6dedc8903f9))
## [7.2.7](https://git.datacontroller.io/dc/dc/compare/v7.2.6...v7.2.7) (2026-02-05)
### Bug Fixes
* dclib not found error in getchangeinfo job ([86791db](https://git.datacontroller.io/dc/dc/commit/86791dbaca39034a19bf8f34efbddf898c57f2f7))
## [7.2.6](https://git.datacontroller.io/dc/dc/compare/v7.2.5...v7.2.6) (2026-01-05)
### Bug Fixes
* **deps:** update angular and moment ([8c5b357](https://git.datacontroller.io/dc/dc/commit/8c5b357dd286db331a6dcdeb3fd499fe3b634288))
## [7.2.5](https://git.datacontroller.io/dc/dc/compare/v7.2.4...v7.2.5) (2025-12-09)
### Bug Fixes
* (build) rebuilt package-lock files ([bfbfd55](https://git.datacontroller.io/dc/dc/commit/bfbfd55fe7e2dff3ce707763a2c7939ff365318b))
* (deps) bump @sasjs/cli and @sasjs/core ([d7c7302](https://git.datacontroller.io/dc/dc/commit/d7c7302c12ac60f355ab9b3b1b461fcf7d0719b8))
* (deps) bumped @sasjs/core, @sasjs/cli, @sasjs/utils and @sasjs/adapter ([af1657e](https://git.datacontroller.io/dc/dc/commit/af1657e226a4efd22cc87401a3850c4a665c2680))
* configurable audit table on restore check ([26ce95f](https://git.datacontroller.io/dc/dc/commit/26ce95f7c1d2260f81c240cd6b058db154d997e4)), closes [#193](https://git.datacontroller.io/dc/dc/issues/193)
* improved testing ([fb3c49a](https://git.datacontroller.io/dc/dc/commit/fb3c49aa8bfdc6acf2ae3034b885010dcdce32a6))
* output values to intended macro variables ([43ae73c](https://git.datacontroller.io/dc/dc/commit/43ae73c5f3ad919394201f54984b61bb2a52fcfe))
## [7.2.4](https://git.datacontroller.io/dc/dc/compare/v7.2.3...v7.2.4) (2025-10-14)
### Bug Fixes
* ensure reload after applying licence key ([cb1978b](https://git.datacontroller.io/dc/dc/commit/cb1978bcaf23b0bf45b5d3b78b9707fd4e48a5f4))
* snyk report security patches ([387f512](https://git.datacontroller.io/dc/dc/commit/387f5122f1ea6dff55d23c9223f17737283a94d3))
## [7.2.3](https://git.datacontroller.io/dc/dc/compare/v7.2.2...v7.2.3) (2025-10-02)
### Bug Fixes
* opening second table in viewer throws an error ([6c6b1cb](https://git.datacontroller.io/dc/dc/commit/6c6b1cbf460e5291ec746af017e764b894fff8d5))
## [7.2.2](https://git.datacontroller.io/dc/dc/compare/v7.2.1...v7.2.2) (2025-09-23) ## [7.2.2](https://git.datacontroller.io/dc/dc/compare/v7.2.1...v7.2.2) (2025-09-23)
+17 -32
View File
@@ -41,6 +41,8 @@
"zone.js", "zone.js",
"text-encoding", "text-encoding",
"crypto-js/md5", "crypto-js/md5",
"crypto-js/sha1",
"crypto-js/sha512",
"buffer", "buffer",
"numbro", "numbro",
"@clr/icons", "@clr/icons",
@@ -51,26 +53,23 @@
"base64-arraybuffer", "base64-arraybuffer",
"@handsontable/formulajs" "@handsontable/formulajs"
], ],
"polyfills": [ "polyfills": ["src/polyfills.ts", "zone.js"],
"src/polyfills.ts",
"zone.js"
],
"outputPath": "dist", "outputPath": "dist",
"resourcesOutputPath": "images",
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": [ "assets": [
"src/images" {
"glob": "**/*",
"input": "src/images",
"output": "images",
"ignore": ["spinner.svg", "caret.svg"]
}
], ],
"styles": [ "styles": ["src/styles.scss"],
"src/styles.scss" "scripts": ["node_modules/marked/marked.min.js"],
], "webWorkerTsConfig": "tsconfig.worker.json",
"scripts": [ "main": "src/main.ts"
"node_modules/marked/marked.min.js"
],
"webWorkerTsConfig": "tsconfig.worker.json"
}, },
"configurations": { "configurations": {
"production": { "production": {
@@ -103,9 +102,7 @@
} }
}, },
"development": { "development": {
"vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true, "sourceMap": true,
"optimization": false, "optimization": false,
"namedChunks": true "namedChunks": true
@@ -134,20 +131,11 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"polyfills": [ "polyfills": ["src/polyfills.ts", "zone.js", "zone.js/testing"],
"src/polyfills.ts",
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": [ "assets": ["src/favicon.ico", "src/assets"],
"src/favicon.ico", "styles": ["src/styles.scss"],
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": [], "scripts": [],
"karmaConfig": "karma.conf.js", "karmaConfig": "karma.conf.js",
"webWorkerTsConfig": "tsconfig.worker.json" "webWorkerTsConfig": "tsconfig.worker.json"
@@ -156,10 +144,7 @@
"lint": { "lint": {
"builder": "@angular-eslint/builder:lint", "builder": "@angular-eslint/builder:lint",
"options": { "options": {
"lintFilePatterns": [ "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
"src/**/*.ts",
"src/**/*.html"
]
} }
} }
} }
+18 -17
View File
@@ -1,13 +1,13 @@
import { defineConfig } from "cypress"; import { defineConfig } from 'cypress'
export default defineConfig({ export default defineConfig({
reporter: "mochawesome", reporter: 'mochawesome',
reporterOptions: { reporterOptions: {
reportDir: "cypress/results", reportDir: 'cypress/results',
overwrite: false, overwrite: false,
html: true, html: true,
json: false, json: false
}, },
viewportHeight: 900, viewportHeight: 900,
viewportWidth: 1600, viewportWidth: 1600,
@@ -16,24 +16,25 @@ export default defineConfig({
defaultCommandTimeout: 30000, defaultCommandTimeout: 30000,
env: { env: {
hosturl: "http://localhost:4200", hosturl: 'http://localhost:4200',
appLocation: "", appLocation: '',
site_id_SAS9: "70221618", site_id_SAS9: '70221618',
site_id_SASVIYA: "70253615", site_id_SASVIYA: '70253615',
site_id_SASJS: "123", site_id_SASJS: '123',
serverType: "SASJS", serverType: 'SASJS',
libraryToOpenIncludes_SASVIYA: "viya", libraryToOpenIncludes_SASVIYA: 'viya',
libraryToOpenIncludes_SAS9: "dc", libraryToOpenIncludes_SAS9: 'dc',
libraryToOpenIncludes_SASJS: "dc", libraryToOpenIncludes_SASJS: 'dc',
debug: false, debug: false,
screenshotOnRunFailure: false, screenshotOnRunFailure: false,
longerCommandTimeout: 50000, longerCommandTimeout: 50000,
testLicenceUserLimits: false, testLicenceUserLimits: false
}, },
e2e: { e2e: {
video: true,
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
// implement node event listeners here // 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}`)
}
+77
View File
@@ -309,6 +309,83 @@ context('excel tests: ', function () {
}) })
}) })
it('22 | Uploads password protected Excel and unlocks with correct password', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
cy.get('.buttonBar button:last-child')
.should('exist')
.click()
.then(() => {
cy.get('input[type="file"]#file-upload')
.attachFile(`/${fixturePath}/regular_excel_password.xlsx`)
.then(() => {
// Wait for password modal to appear
cy.get('#filePasswordInput', { timeout: 10000 })
.should('be.visible')
.type('123123')
// Click Unlock button
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
// Click away the overlay
cy.get('.modal-footer .btn.btn-primary', { timeout: 5000 }).click()
// Verify file loads successfully
cy.get('.btn-upload-preview', { timeout: 60000 })
.should('be.visible')
.then(() => {
submitExcel()
rejectExcel(done)
})
})
})
})
it('23 | Uploads password protected Excel and handles wrong password', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
cy.get('.buttonBar button:last-child')
.should('exist')
.click()
.then(() => {
cy.get('input[type="file"]#file-upload')
.attachFile(`/${fixturePath}/regular_excel_password.xlsx`)
.then(() => {
// First attempt: Enter wrong password
cy.get('#filePasswordInput', { timeout: 10000 })
.should('be.visible')
.type('wrongpassword')
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
// Verify error message appears
cy.get('.modal-footer .color-red', { timeout: 10000 })
.should('be.visible')
.should('contain', "Sorry that didn't work, try again.")
// Modal should still be open for retry
cy.get('#filePasswordInput')
.should('be.visible')
.clear()
.type('123123')
// Second attempt: Enter correct password
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
// Click away the overlay
cy.get('.modal-footer .btn.btn-primary', { timeout: 5000 }).click()
// Verify file loads successfully
cy.get('.btn-upload-preview', { timeout: 60000 })
.should('be.visible')
.then(() => {
submitExcel()
rejectExcel(done)
})
})
})
})
// Large files break Cypress // Large files break Cypress
// it ('? | Uploads Excel with size of 5MB', (done) => { // it ('? | Uploads Excel with size of 5MB', (done) => {
+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 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 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 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 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 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 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
Binary file not shown.
+1 -1
View File
@@ -10,7 +10,7 @@ const check = (cwd) => {
onlyAllow: 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;', '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: excludePackages:
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;handsontable@16.0.1;hyperformula@2.7.1;hyperformula@3.0.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) => { (error, json) => {
if (error) { if (error) {
+6 -2
View File
@@ -2,8 +2,8 @@ module.exports = {
ci: { ci: {
collect: { collect: {
settings: { settings: {
preset: "desktop", preset: 'desktop',
chromeFlags: "--no-sandbox --disable-dev-shm-usage" chromeFlags: '--no-sandbox --disable-dev-shm-usage'
}, },
url: [ url: [
'http://localhost:5000/AppStream/clickme/#/home/tables', 'http://localhost:5000/AppStream/clickme/#/home/tables',
@@ -37,6 +37,10 @@ module.exports = {
{ minScore: 0.4, aggregationMethod: 'median' } { minScore: 0.4, aggregationMethod: 'median' }
] ]
} }
},
upload: {
target: 'filesystem',
outputDir: './lighthouse-reports'
} }
} }
} }
+8898 -7019
View File
File diff suppressed because it is too large Load Diff
+38 -34
View File
@@ -23,7 +23,7 @@
"watch": "ng test watch=true", "watch": "ng test watch=true",
"pree2e": "webdriver-manager update", "pree2e": "webdriver-manager update",
"e2e": "protractor protractor.config.js", "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", "add-githook": "[ -d ../.git ] && git config core.hooksPath ./.git-hooks || true",
"cypress": "cypress open", "cypress": "cypress open",
"cy:run": "cypress run", "cy:run": "cypress run",
@@ -32,26 +32,27 @@
"compodoc:build": "compodoc -p tsconfig.doc.json --name 'Data Controller Client'", "compodoc:build": "compodoc -p tsconfig.doc.json --name 'Data Controller Client'",
"compodoc:build-and-serve": "compodoc -p tsconfig.doc.json -s --name 'Data Controller Client'", "compodoc:build-and-serve": "compodoc -p tsconfig.doc.json -s --name 'Data Controller Client'",
"compodoc:serve": "compodoc -s --name 'Data Controller Client'", "compodoc:serve": "compodoc -s --name 'Data Controller Client'",
"lighthouse": "lhci autorun" "lighthouse": "lhci autorun",
"ng": "ng"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^17.3.3", "@angular/animations": "^19.2.20",
"@angular/cdk": "^17.3.3", "@angular/cdk": "^19.2.19",
"@angular/common": "^17.3.3", "@angular/common": "^19.2.20",
"@angular/compiler": "^17.3.3", "@angular/compiler": "^19.2.20",
"@angular/core": "^17.3.3", "@angular/core": "^19.2.20",
"@angular/forms": "^17.3.3", "@angular/forms": "^19.2.20",
"@angular/platform-browser": "^17.3.3", "@angular/platform-browser": "^19.2.20",
"@angular/platform-browser-dynamic": "^17.3.3", "@angular/platform-browser-dynamic": "^19.2.20",
"@angular/router": "^17.3.3", "@angular/router": "^19.2.20",
"@cds/core": "^6.15.1", "@cds/core": "^6.15.1",
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz", "@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
"@clr/icons": "^13.0.2", "@clr/icons": "^13.0.2",
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz", "@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
"@handsontable/angular-wrapper": "16.0.1", "@handsontable/angular-wrapper": "^17.1.0",
"@sasjs/adapter": "^4.12.2", "@sasjs/adapter": "^4.17.0",
"@sasjs/utils": "^3.4.0", "@sasjs/utils": "^3.5.3",
"@sheet/crypto": "file:libraries/sheet-crypto.tgz", "@sheet/crypto": "file:libraries/sheet-crypto.tgz",
"@types/d3-graphviz": "^2.6.7", "@types/d3-graphviz": "^2.6.7",
"@types/text-encoding": "0.0.35", "@types/text-encoding": "0.0.35",
@@ -61,14 +62,14 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"d3-graphviz": "^5.0.2", "d3-graphviz": "^5.0.2",
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"handsontable": "^16.0.1", "handsontable": "^17.1.0",
"https-browserify": "1.0.0", "https-browserify": "1.0.0",
"hyperformula": "^2.5.0", "hyperformula": "^2.5.0",
"iconv-lite": "^0.5.0", "iconv-lite": "^0.5.0",
"jquery-datetimepicker": "^2.5.21", "jquery-datetimepicker": "^2.5.21",
"jsrsasign": "^11.1.0", "jsrsasign": "11.1.1",
"marked": "^5.0.0", "marked": "^5.0.0",
"moment": "^2.26.0", "moment": "^2.30.1",
"ngx-clipboard": "^16.0.0", "ngx-clipboard": "^16.0.0",
"ngx-json-viewer": "file:libraries/ngx-json-viewer-3.2.1.tgz", "ngx-json-viewer": "file:libraries/ngx-json-viewer-3.2.1.tgz",
"nodejs": "0.0.0", "nodejs": "0.0.0",
@@ -81,22 +82,22 @@
"tslib": "^2.3.0", "tslib": "^2.3.0",
"vm": "^0.1.0", "vm": "^0.1.0",
"webpack": "^5.91.0", "webpack": "^5.91.0",
"xlsx": "^0.18.5", "xlsx": "file:libraries/xlsx-0.20.3.tgz",
"zone.js": "~0.14.4" "zone.js": "~0.15.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.3.3", "@angular-devkit/build-angular": "^19.2.24",
"@angular-eslint/builder": "17.3.0", "@angular-eslint/builder": "19.8.1",
"@angular-eslint/eslint-plugin": "17.3.0", "@angular-eslint/eslint-plugin": "19.8.1",
"@angular-eslint/eslint-plugin-template": "17.3.0", "@angular-eslint/eslint-plugin-template": "19.8.1",
"@angular-eslint/schematics": "17.3.0", "@angular-eslint/schematics": "19.8.1",
"@angular-eslint/template-parser": "17.3.0", "@angular-eslint/template-parser": "19.8.1",
"@angular/cli": "^17.3.3", "@angular/cli": "^19.2.24",
"@angular/compiler-cli": "^17.3.3", "@angular/compiler-cli": "^19.2.20",
"@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-methods": "^7.18.6",
"@compodoc/compodoc": "^1.1.21", "@compodoc/compodoc": "^1.2.1",
"@cypress/webpack-preprocessor": "^5.17.1", "@cypress/webpack-preprocessor": "^5.17.1",
"@lhci/cli": "^0.12.0", "@lhci/cli": "^0.15.1",
"@types/core-js": "^2.5.5", "@types/core-js": "^2.5.5",
"@types/crypto-js": "^4.2.1", "@types/crypto-js": "^4.2.1",
"@types/es6-shim": "^0.31.39", "@types/es6-shim": "^0.31.39",
@@ -104,15 +105,15 @@
"@types/lodash-es": "^4.17.3", "@types/lodash-es": "^4.17.3",
"@types/marked": "^4.3.0", "@types/marked": "^4.3.0",
"@types/node": "12.20.50", "@types/node": "12.20.50",
"@typescript-eslint/eslint-plugin": "^5.29.0", "@typescript-eslint/eslint-plugin": "8.31.1",
"@typescript-eslint/parser": "^5.29.0", "@typescript-eslint/parser": "8.31.1",
"core-js": "^2.5.4", "core-js": "^2.5.4",
"cypress": "12.17.1", "cypress": "^15.14.2",
"cypress-file-upload": "^5.0.8", "cypress-file-upload": "^5.0.8",
"cypress-plugin-tab": "^1.0.5", "cypress-plugin-tab": "^1.0.5",
"cypress-real-events": "^1.8.1", "cypress-real-events": "^1.8.1",
"es6-shim": "^0.35.5", "es6-shim": "^0.35.5",
"eslint": "^8.33.0", "eslint": "8.57.1",
"git-describe": "^4.0.4", "git-describe": "^4.0.4",
"jasmine-core": "~5.1.2", "jasmine-core": "~5.1.2",
"karma": "~6.4.3", "karma": "~6.4.3",
@@ -128,8 +129,11 @@
"rimraf": "3.0.2", "rimraf": "3.0.2",
"ts-loader": "^9.2.8", "ts-loader": "^9.2.8",
"ts-node": "^3.3.0", "ts-node": "^3.3.0",
"typescript": "~5.4.4", "typescript": "~5.8.3",
"wait-on": "^6.0.1", "wait-on": "^6.0.1",
"watch": "^1.0.2" "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 * Cached viyaApi collections, search and selected endpoint
*/ */
export const globals: { export const globals: {
embed: boolean
rootParam: string rootParam: string
dcLib: string dcLib: string
xlmaps: XLMapListItem[] xlmaps: XLMapListItem[]
@@ -69,6 +70,7 @@ export const globals: {
handsontable: HandsontableStaticConfig handsontable: HandsontableStaticConfig
[key: string]: any [key: string]: any
} = { } = {
embed: false,
rootParam: <string>'', rootParam: <string>'',
dcLib: '', dcLib: '',
xlmaps: [], xlmaps: [],
+5 -4
View File
@@ -107,7 +107,7 @@
</div> </div>
</ng-container> </ng-container>
<header class="app-header"> <header class="app-header" *ngIf="!embed">
<!-- <button <!-- <button
*ngIf=" *ngIf="
isMainRoute('view') || isMainRoute('view') ||
@@ -213,9 +213,10 @@
</header> </header>
<nav <nav
*ngIf=" *ngIf="
router.url.includes('submitted') || !embed &&
router.url.includes('approve') || (router.url.includes('submitted') ||
router.url.includes('history') router.url.includes('approve') ||
router.url.includes('history'))
" "
class="subnav" class="subnav"
> >
+15 -2
View File
@@ -11,7 +11,7 @@ import { Location } from '@angular/common'
import '@clr/icons' import '@clr/icons'
import '@clr/icons/shapes/all-shapes' import '@clr/icons/shapes/all-shapes'
import { globals } from './_globals' import { globals } from './_globals'
import * as moment from 'moment' import moment from 'moment'
import { EventService } from './services/event.service' import { EventService } from './services/event.service'
import { AppService } from './services/app.service' import { AppService } from './services/app.service'
import { InfoModal } from './models/InfoModal' import { InfoModal } from './models/InfoModal'
@@ -42,7 +42,8 @@ ClarityIcons.addIcons(
selector: 'my-app', selector: 'my-app',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'], styleUrls: ['./app.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class AppComponent { export class AppComponent {
private dcAdapterSettings: DcAdapterSettings | undefined private dcAdapterSettings: DcAdapterSettings | undefined
@@ -69,6 +70,7 @@ export class AppComponent {
public syssite = this.appService.syssite public syssite = this.appService.syssite
public licenceState = this.licenceService.licenceState public licenceState = this.licenceService.licenceState
public embed = globals.embed
constructor( constructor(
private appService: AppService, private appService: AppService,
@@ -142,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.subscribeToShowAbortModal()
this.subscribeToRequestsModal() this.subscribeToRequestsModal()
this.subscribeToStartupData() this.subscribeToStartupData()
@@ -197,6 +209,7 @@ export class AppComponent {
dcPath: getAppAttribute('dcPath') || '', dcPath: getAppAttribute('dcPath') || '',
debug: getAppAttribute('debug') === 'true' || false, debug: getAppAttribute('debug') === 'true' || false,
useComputeApi: this.parseComputeApi(getAppAttribute('useComputeApi')), useComputeApi: this.parseComputeApi(getAppAttribute('useComputeApi')),
runAsTask: getAppAttribute('runAsTask') === 'true' || false,
contextName: getAppAttribute('contextName') || '', contextName: getAppAttribute('contextName') || '',
hotLicenceKey: getAppAttribute('hotLicenceKey') || '' hotLicenceKey: getAppAttribute('hotLicenceKey') || ''
} }
+9 -4
View File
@@ -1,4 +1,4 @@
import { HttpClientModule } from '@angular/common/http' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { BrowserModule } from '@angular/platform-browser' import { BrowserModule } from '@angular/platform-browser'
@@ -36,12 +36,12 @@ import { AppSettingsService } from './services/app-settings.service'
InfoModalComponent, InfoModalComponent,
ViyaApiExplorerComponent ViyaApiExplorerComponent
], ],
bootstrap: [AppComponent],
imports: [ imports: [
BrowserAnimationsModule, BrowserAnimationsModule,
BrowserModule, BrowserModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
HttpClientModule,
ROUTING, ROUTING,
SharedModule, SharedModule,
ClarityModule, ClarityModule,
@@ -50,7 +50,12 @@ import { AppSettingsService } from './services/app-settings.service'
DirectivesModule, DirectivesModule,
NgxJsonViewerModule NgxJsonViewerModule
], ],
providers: [AppService, SasStoreService, LicensingGuard, AppSettingsService], providers: [
bootstrap: [AppComponent] AppService,
SasStoreService,
LicensingGuard,
AppSettingsService,
provideHttpClient(withInterceptorsFromDi())
]
}) })
export class AppModule {} export class AppModule {}
+2 -1
View File
@@ -14,7 +14,8 @@ import { DcAdapterSettings } from '../models/DcAdapterSettings'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class DeployComponent implements OnInit { export class DeployComponent implements OnInit {
public step: number = 0 public step: number = 0
@@ -29,7 +29,8 @@ import {
selector: 'app-automatic-deploy', selector: 'app-automatic-deploy',
templateUrl: './automatic.component.html', templateUrl: './automatic.component.html',
styleUrls: ['./automatic.component.scss'], styleUrls: ['./automatic.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class AutomaticComponent implements OnInit { export class AutomaticComponent implements OnInit {
@Input() sasJs!: SASjs @Input() sasJs!: SASjs
@@ -372,7 +373,7 @@ export class AutomaticComponent implements OnInit {
let contextname = `&_contextname=${params.contextName}` let contextname = `&_contextname=${params.contextName}`
let admin = `&admin=${params.admin}` let admin = `&admin=${params.admin}`
let dcPath = `&dcpath=${params.dcPath}` let dcPath = `&dcpath=${params.dcPath}`
let debug = `&_debug=131` let debug = this.sasService.getDebugUrlParam()
let programUrl = let programUrl =
serverUrl + serverUrl +
@@ -18,7 +18,8 @@ import { SasService } from 'src/app/services/sas.service'
selector: 'app-manual-deploy', selector: 'app-manual-deploy',
templateUrl: './manual.component.html', templateUrl: './manual.component.html',
styleUrls: ['./manual.component.scss'], styleUrls: ['./manual.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ManualComponent implements OnInit { export class ManualComponent implements OnInit {
@Input() sasJs!: SASjs @Input() sasJs!: SASjs
@@ -250,7 +251,7 @@ export class ManualComponent implements OnInit {
this.selectedAdminGroup + this.selectedAdminGroup +
'&DCPATH=' + '&DCPATH=' +
this.dcPath + this.dcPath +
'&_debug=131' this.sasService.getDebugUrlParam()
window.open(url, '_blank') window.open(url, '_blank')
@@ -20,7 +20,8 @@ import { SasjsService } from 'src/app/services/sasjs.service'
selector: 'app-sasjs-configurator', selector: 'app-sasjs-configurator',
templateUrl: './sasjs-configurator.component.html', templateUrl: './sasjs-configurator.component.html',
styleUrls: ['./sasjs-configurator.component.scss'], styleUrls: ['./sasjs-configurator.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class SasjsConfiguratorComponent implements OnInit { export class SasjsConfiguratorComponent implements OnInit {
@Input() sasJs!: SASjs @Input() sasJs!: SASjs
@@ -7,7 +7,8 @@ import {
} from '@angular/core' } from '@angular/core'
@Directive({ @Directive({
selector: '[appDragNdrop]' selector: '[appDragNdrop]',
standalone: false
}) })
export class DragNdropDirective { export class DragNdropDirective {
@HostBinding('class.fileover') fileOver: boolean = false @HostBinding('class.fileover') fileOver: boolean = false
@@ -9,7 +9,8 @@ import {
import { FileUploader } from '../models/FileUploader.class' import { FileUploader } from '../models/FileUploader.class'
@Directive({ @Directive({
selector: '[appFileDrop]' selector: '[appFileDrop]',
standalone: false
}) })
export class FileDropDirective { export class FileDropDirective {
@Input() uploader?: FileUploader @Input() uploader?: FileUploader
@@ -9,7 +9,8 @@ import {
import { FileUploader } from '../models/FileUploader.class' import { FileUploader } from '../models/FileUploader.class'
@Directive({ @Directive({
selector: '[appFileSelect]' selector: '[appFileSelect]',
standalone: false
}) })
export class FileSelectDirective { export class FileSelectDirective {
@Input() uploader?: FileUploader @Input() uploader?: FileUploader
@@ -6,7 +6,8 @@ import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'
* Calling functions in html is bad for performance * Calling functions in html is bad for performance
*/ */
@Directive({ @Directive({
selector: '[ngVar]' selector: '[ngVar]',
standalone: false
}) })
export class NgVarDirective { export class NgVarDirective {
@Input() @Input()
@@ -1,7 +1,8 @@
import { Directive, HostListener } from '@angular/core' import { Directive, HostListener } from '@angular/core'
@Directive({ @Directive({
selector: '[appStealFocus]' selector: '[appStealFocus]',
standalone: false
}) })
export class StealFocusDirective { export class StealFocusDirective {
constructor() {} constructor() {}
@@ -14,6 +14,7 @@ import { HelperService } from 'src/app/services/helper.service'
import { SasStoreService } from 'src/app/services/sas-store.service' import { SasStoreService } from 'src/app/services/sas-store.service'
import { DcValidator } from 'src/app/shared/dc-validator/dc-validator' import { DcValidator } from 'src/app/shared/dc-validator/dc-validator'
import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model' import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model'
import { isEmpty } from 'src/app/shared/dc-validator/utils/isEmpty'
import { import {
EditRecordDropdownChangeEvent, EditRecordDropdownChangeEvent,
EditRecordInputFocusedEvent EditRecordInputFocusedEvent
@@ -24,7 +25,8 @@ import { EditRecordModal } from '../../models/EditRecordModal'
selector: 'app-edit-record', selector: 'app-edit-record',
templateUrl: './edit-record.component.html', templateUrl: './edit-record.component.html',
styleUrls: ['./edit-record.component.scss'], styleUrls: ['./edit-record.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class EditRecordComponent implements OnInit { export class EditRecordComponent implements OnInit {
@Input() currentRecord!: EditRecordModal @Input() currentRecord!: EditRecordModal
@@ -99,7 +101,7 @@ export class EditRecordComponent implements OnInit {
let format = cellValidation ? cellValidation.dateFormat : '' let format = cellValidation ? cellValidation.dateFormat : ''
if (this.currentRecord) if (this.currentRecord)
this.currentRecord[colKey] = moment(date).format(format) this.currentRecord[colKey] = moment(date).format(format as string)
} }
/** /**
@@ -145,23 +147,63 @@ export class EditRecordComponent implements OnInit {
}, 0) }, 0)
} }
async recordInputChange(event: any, colName: string) { async recordInputChange(event: any, colName: string): Promise<void> {
const colRules = this.currentRecordValidator?.getRule(colName) const colRules = this.currentRecordValidator?.getRule(colName)
const value = event.target.value const value = event.target.value
this.helperService.debounceCall(300, () => { this.helperService.debounceCall(300, () => {
this.validateRecordCol(colRules, value).then((valid: boolean) => { this.validateRecordCol(colRules, value).then((valid: boolean) => {
const index = this.currentRecordInvalidCols.indexOf(colName) this.updateValidationState(colName, valid)
if (valid) { if (!valid) {
if (index > -1) this.currentRecordInvalidCols.splice(index, 1) this.tryAutoPopulateNotNull(event, colName, colRules, value)
} else {
if (index < 0) this.currentRecordInvalidCols.push(colName)
} }
}) })
}) })
} }
/**
* Updates the invalid columns list based on validation result
*/
private updateValidationState(colName: string, valid: boolean): void {
const index = this.currentRecordInvalidCols.indexOf(colName)
if (valid && index > -1) {
this.currentRecordInvalidCols.splice(index, 1)
} else if (!valid && index < 0) {
this.currentRecordInvalidCols.push(colName)
}
}
/**
* Auto-populates NOTNULL default value when the field is empty and has a default
*/
private tryAutoPopulateNotNull(
event: any,
colName: string,
colRules: DcValidation | undefined,
value: any
): void {
if (
!isEmpty(value) ||
!this.currentRecordValidator ||
!this.currentRecord
) {
return
}
const defaultValue =
this.currentRecordValidator.getNotNullDefaultValue(colName)
if (defaultValue === undefined) return
this.currentRecord[colName] = defaultValue
event.target.value = defaultValue
this.validateRecordCol(colRules, defaultValue).then((isValid: boolean) => {
this.updateValidationState(colName, isValid)
})
}
onNextRecordClick() { onNextRecordClick() {
this.onNextRecord.emit() this.onNextRecord.emit()
} }
@@ -171,23 +213,8 @@ export class EditRecordComponent implements OnInit {
} }
public copyToClip(text: string) { public copyToClip(text: string) {
const modalElement = document.querySelector('#recordModalRef .modal-title') navigator.clipboard.writeText(text)
this.generatedRecordUrl = text
if (modalElement) {
const selBox = document.createElement('textarea')
selBox.style.position = 'fixed'
selBox.style.left = '0'
selBox.style.top = '0'
selBox.style.opacity = '0'
selBox.style.zIndex = '5000'
selBox.value = text
modalElement.appendChild(selBox)
selBox.focus()
selBox.select()
document.execCommand('copy')
modalElement.removeChild(selBox)
this.generatedRecordUrl = text
}
} }
async generateEditRecordUrl() { async generateEditRecordUrl() {
@@ -11,7 +11,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
selector: 'app-upload-stater', selector: 'app-upload-stater',
templateUrl: './upload-stater.component.html', templateUrl: './upload-stater.component.html',
styleUrls: ['./upload-stater.component.scss'], styleUrls: ['./upload-stater.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class UploadStaterComponent implements OnInit { export class UploadStaterComponent implements OnInit {
public statesList: string[] = [] //States appended to be displayed public statesList: string[] = [] //States appended to be displayed
+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" class="card-header clr-row buttonBar headerBar clr-flex-md-row clr-justify-content-center clr-justify-content-lg-end"
> >
<div <div
*ngIf="tableTrue" *ngIf="tableTrue && !embed"
class="clr-col-12 clr-col-md-3 clr-col-lg-4 backBtn" class="clr-col-12 clr-col-md-3 clr-col-lg-4 backBtn"
> >
<span <span
@@ -873,3 +873,17 @@
</app-dataset-info> </app-dataset-info>
<app-viewboxes [(viewboxModal)]="viewboxes"></app-viewboxes> <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>
+423 -70
View File
@@ -13,6 +13,7 @@ import {
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import Handsontable from 'handsontable' import Handsontable from 'handsontable'
import { Subject, Subscription } from 'rxjs' import { Subject, Subscription } from 'rxjs'
import { sanitiseForSas } from '../shared/utils/sanitise'
import { SasStoreService } from '../services/sas-store.service' import { SasStoreService } from '../services/sas-store.service'
type AOA = any[][] type AOA = any[][]
@@ -43,6 +44,8 @@ import { Col } from '../shared/dc-validator/models/col.model'
import { DcValidation } from '../shared/dc-validator/models/dc-validation.model' import { DcValidation } from '../shared/dc-validator/models/dc-validation.model'
import { DQRule } from '../shared/dc-validator/models/dq-rules.model' import { DQRule } from '../shared/dc-validator/models/dq-rules.model'
import { getHotDataSchema } from '../shared/dc-validator/utils/getHotDataSchema' 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 { globals } from '../_globals'
import { UploadStaterComponent } from './components/upload-stater/upload-stater.component' import { UploadStaterComponent } from './components/upload-stater/upload-stater.component'
import { DynamicExtendedCellValidation } from './models/dynamicExtendedCellValidation' import { DynamicExtendedCellValidation } from './models/dynamicExtendedCellValidation'
@@ -70,7 +73,8 @@ import { ParseResult } from '../models/ParseResult.interface'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChildren('uploadStater') @ViewChildren('uploadStater')
@@ -130,7 +134,9 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
licenseKey: this.hotTable.licenseKey, licenseKey: this.hotTable.licenseKey,
readOnly: this.hotTable.readOnly, readOnly: this.hotTable.readOnly,
copyPaste: this.hotTable.copyPaste, copyPaste: this.hotTable.copyPaste,
contextMenu: true contextMenu: true,
className: 'htDark',
theme: 'ht-theme-classic'
} }
} }
@@ -262,6 +268,9 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
public badEdit = false public badEdit = false
public badEditCause: string | undefined public badEditCause: string | undefined
public badEditTitle: string | undefined public badEditTitle: string | undefined
get embed() {
return globals.embed
}
public tableTrue: boolean | undefined public tableTrue: boolean | undefined
public saveLoading = false public saveLoading = false
public approvers: string[] = [] public approvers: string[] = []
@@ -351,7 +360,29 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
* Hash/values table used for dynamic cell validation * Hash/values table used for dynamic cell validation
*/ */
public cellValidationSource: CellValidationSource[] = [] 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: { public extendedCellValidationFields: {
DISPLAY_INDEX: number DISPLAY_INDEX: number
EXTRA_COL_NAME: number EXTRA_COL_NAME: number
@@ -957,6 +988,8 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
cancelEdit() { cancelEdit() {
this.cancelBulkValidation({ revert: false })
this.toggleHotPlugin('contextMenu', false) this.toggleHotPlugin('contextMenu', false)
this.cellValidationSource = [] this.cellValidationSource = []
@@ -986,7 +1019,8 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
false false
) )
hot.validateRows(this.modifedRowsIndexes) this.modifedRowsIndexes = []
hot.validateCells()
// this.editRecordListeners(); // this.editRecordListeners();
for (const sortConfig of sortConfigs) { for (const sortConfig of sortConfigs) {
columnSorting.sort(sortConfig) columnSorting.sort(sortConfig)
@@ -995,6 +1029,160 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.checkRowLimit() 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 timesClicked = 0
public hotClicked() { public hotClicked() {
if (this.timesClicked === 1 && this.hotTable.readOnly) { if (this.timesClicked === 1 && this.hotTable.readOnly) {
@@ -1044,12 +1232,16 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
/** /**
* Creates a new empty row object with proper structure * Creates a new empty row object with proper structure.
* Columns with NOTNULL DQ rules are pre-populated with their RULE_VALUE.
*/ */
private createEmptyRow(): any { private createEmptyRow(): any {
const newRow: any = {} const newRow: any = {}
this.headerColumns.forEach((col: string) => { this.cellValidation.forEach((rule: any) => {
newRow[col] = '' const dataKey = rule.data
newRow[dataKey] = this.hotDataSchema.hasOwnProperty(dataKey)
? this.hotDataSchema[dataKey]
: ''
}) })
newRow['noLinkOption'] = true newRow['noLinkOption'] = true
return newRow return newRow
@@ -1660,7 +1852,7 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.submit = true this.submit = true
const updateParams: any = {} const updateParams: any = {}
updateParams.ACTION = 'LOAD' updateParams.ACTION = 'LOAD'
this.message = this.message.replace(/\n/g, '. ') this.message = sanitiseForSas(this.message.replace(/\n/g, '. '))
updateParams.MESSAGE = this.message updateParams.MESSAGE = this.message
// updateParams.APPROVER = this.approver; // updateParams.APPROVER = this.approver;
updateParams.LIBDS = this.libds updateParams.LIBDS = this.libds
@@ -1959,18 +2151,26 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
* @param row handsontable row * @param row handsontable row
* @param column handsontable column * @param column handsontable column
*/ */
public dynamicCellValidation(row: number, column: number) { public async dynamicCellValidation(
if (this.dynamicCellValidationDisabled(row, column)) return row: number,
column: number,
opts?: { skipRender?: boolean },
retried = false
): Promise<void> {
if (this.dynamicCellValidationDisabled(row, column))
return Promise.resolve()
const hot = this.hotInstance const hot = this.hotInstance
const cellMeta = hot.getCellMeta(row, column) const cellMeta = hot.getCellMeta(row, column)
if (cellMeta.readOnly) return if (cellMeta.readOnly) return Promise.resolve()
const cellData = hot.getDataAtCell(row, column) const cellData = hot.getDataAtCell(row, column)
const clickedRow = this.helperService.deepClone(this.dataSource[row]) const clickedRow = this.helperService.deepClone(this.dataSource[row])
const clickedColumnKey = Object.keys(clickedRow)[column] 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 * We will hash the row (without current column) so later we check if hash is the same
@@ -1991,6 +2191,22 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
* Set the values for found hash. * 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) { if (validationSourceIndex > -1) {
let colSource = this.cellValidationSource[ let colSource = this.cellValidationSource[
validationSourceIndex validationSourceIndex
@@ -2066,14 +2282,12 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
cellHadSource && cellHasValue cellHadSource && cellHasValue
) )
hot.render() if (!skipRender) hot.render()
}) })
} } else if (validationSourceIndex < 0) {
/**
/** * Send request to sas.
* Send request to sas. */
*/
if (validationSourceIndex < 0) {
const data = { const data = {
SASControlTable: [ SASControlTable: [
{ {
@@ -2105,21 +2319,53 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
count: this.cellValidationSource.length + 1 count: this.cellValidationSource.length + 1
}) })
hot.setCellMeta(row, column, 'renderer', spinnerRenderer)
this.currentEditRecordLoadings.push(column) 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, { .request('editors/getdynamiccolvals', data, undefined, {
suppressSuccessAbortModal: true, suppressSuccessAbortModal: true,
suppressErrorAbortModal: 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( const colSource = res.adapterResponse.dynamic_values.map(
(el: any) => el[this.cellValidationFields.RAW_VALUE] (el: any) => el[this.cellValidationFields.RAW_VALUE]
) )
this.currentEditRecordLoadings.splice(
this.currentEditRecordLoadings.indexOf(column),
1
)
if (colSource.length > 0) { if (colSource.length > 0) {
const validationSourceIndex = this.cellValidationSource.findIndex( const validationSourceIndex = this.cellValidationSource.findIndex(
(entry: CellValidationSource) => entry.hash === hashedRow (entry: CellValidationSource) => entry.hash === hashedRow
@@ -2131,49 +2377,37 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
row: row, row: row,
col: column, col: column,
values: res.adapterResponse.dynamic_values, 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 * In the case that the original value is not included in the newly created cell dropdown
* we need to update it here after we set new `cellValidationSource` (validation lookup hash table) values * and validation type is HARDSELECT, the cell shoud be red
* so that it will check those values to decide whether numeric cells should be
* converted to the dropdown
*/ */
await new Promise<void>((resolve) =>
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
*/
setTimeout(() => { setTimeout(() => {
this.reSetCellValidationValues(true, row) this.reSetCellValidationValues(true, row)
hot.render()
hot.validateRows([row]) if (!skipRender) {
hot.render()
hot.validateRows([row])
}
resolve()
}, 100) }, 100)
}) )
} else {
if (!skipRender) {
hot.setCellMeta(row, column, 'renderer', noSpinnerRenderer)
hot.render()
}
const idx = this.cellValidationSource.findIndex(
(e) => e.hash === hashedRow
)
if (idx > -1) this.cellValidationSource[idx].pending = undefined
} }
//Removing the spinner from cell, so validation not fail
hot.setCellMeta(row, column, 'renderer', noSpinnerRenderer)
this.currentEditRecordLoadings.splice(
this.currentEditRecordLoadings.indexOf(column),
1
)
hot.deselectCell()
hot.render()
/** /**
* If hash table limit reached, remove the oldest element. * If hash table limit reached, remove the oldest element.
* Oldest element is element with lowest `count` number. * Oldest element is element with lowest `count` number.
@@ -2189,18 +2423,25 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
}) })
.catch((err: any) => { .catch((err: any) => {
if (spinnerTimeout) clearTimeout(spinnerTimeout)
this.pendingSpinnerCells.delete(spinnerKey)
const currentRowHashIndex = this.cellValidationSource.findIndex( const currentRowHashIndex = this.cellValidationSource.findIndex(
(x) => x.hash === hashedRow (x) => x.hash === hashedRow
) )
this.cellValidationSource.splice(currentRowHashIndex, 1) this.cellValidationSource.splice(currentRowHashIndex, 1)
hot.batch(() => { if (myEpoch !== this.validationEpoch) return
// Render error icon inside a cell
hot.setCellMeta(row, column, 'renderer', errorRenderer)
hot.render() if (!skipRender) {
}) hot.batch(() => {
// Render error icon inside a cell
hot.setCellMeta(row, column, 'renderer', errorRenderer)
hot.render()
})
}
//Stop edit record modal loading spinner //Stop edit record modal loading spinner
this.currentEditRecordLoadings.splice( this.currentEditRecordLoadings.splice(
@@ -2213,8 +2454,10 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
// After waiting time remove the error icon from cell and edit record modal field // After waiting time remove the error icon from cell and edit record modal field
setTimeout(() => { setTimeout(() => {
hot.setCellMeta(row, column, 'renderer', noSpinnerRenderer) if (!skipRender) {
hot.render() hot.setCellMeta(row, column, 'renderer', noSpinnerRenderer)
hot.render()
}
//Remove error icon on the edit record modal field //Remove error icon on the edit record modal field
this.currentEditRecordErrors.splice( this.currentEditRecordErrors.splice(
@@ -2227,8 +2470,19 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.loggerService.log('getdynamiccolvals error:', err) 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() { checkEmptyRowWhenFilter() {
@@ -2675,13 +2929,14 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
// Note: this.headerColumns and this.columnHeader contains same data // Note: this.headerColumns and this.columnHeader contains same data
// need to resolve redundancy // need to resolve redundancy
// default schema // default schema - includes NOTNULL defaults from DQ rules
for (let i = 0; i < this.headerColumns.length; i++) { for (let i = 0; i < this.headerColumns.length; i++) {
const colType = this.cellValidation[i].type const colType = this.cellValidation[i].type
this.hotDataSchema[this.cellValidation[i].data] = getHotDataSchema( this.hotDataSchema[this.cellValidation[i].data] = getHotDataSchema(
colType, colType,
this.cellValidation[i] this.cellValidation[i],
this.dcValidator?.getDqDetails()
) )
} }
@@ -2927,6 +3182,31 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.setCellFilter(true) 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) => { hot.addHook('afterChange', (source: any, change: any) => {
if (change === 'edit') { if (change === 'edit') {
const hot = this.hotInstance const hot = this.hotInstance
@@ -2945,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) => { hot.addHook('afterRender', (isForced: boolean) => {
this.eventService.dispatchEvent('resize') this.eventService.dispatchEvent('resize')
@@ -2986,21 +3298,62 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
) )
hot.addHook('beforePaste', (data: any, cords: any) => { // Auto-populate NOTNULL default when validation fails due to empty value
const startCol = cords[0].startCol hot.addHook(
'afterValidate',
(isValid: boolean, value: any, row: number, prop: string | number) => {
if (isValid || !isEmpty(value)) return
// We iterate trough pasting data to convert to numbers if needed const colName =
data[0] = data[0].map((value: any, index: number) => { typeof prop === 'string'
? prop
: (hot.colToProp(prop as number) as string)
const defaultValue = this.dcValidator?.getNotNullDefaultValue(colName)
if (defaultValue === undefined) return
// Auto-populate using setTimeout to avoid modifying during validation
setTimeout(() => {
if (isEmpty(hot.getDataAtRowProp(row, colName))) {
hot.setDataAtRowProp(row, colName, defaultValue, 'autoPopulate')
}
}, 0)
}
)
// 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 colName = this.columnHeader[startCol + index]
const isColNum = this.$dataFormats?.vars[colName]?.type === 'num' const isColNum = this.$dataFormats?.vars[colName]?.type === 'num'
const specialMissing = isSpecialMissing(value) const specialMissing = isSpecialMissing(value)
if (isColNum && !isNaN(value) && !specialMissing) value = value * 1 if (isColNum && !isNaN(value) && !specialMissing) value = value * 1
return value 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', () => { hot.addHook('afterRemoveRow', () => {
this.checkRowLimit() 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 * Custom renderer for HOT cell
* Used to show error icon * Used to show error icon
+1 -1
View File
@@ -30,7 +30,7 @@ export const freeTierConfig: LicenceState = {
lineage_daily_limit: 3, lineage_daily_limit: 3,
tables_in_library_limit: 35, tables_in_library_limit: 35,
viewbox: true, viewbox: true,
fileUpload: true, fileUpload: false,
editRecord: true, editRecord: true,
addRecord: true addRecord: true
} }
+2 -1
View File
@@ -15,7 +15,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class GroupComponent implements OnInit { export class GroupComponent implements OnInit {
public groups: Array<any> | undefined public groups: Array<any> | undefined
+2 -1
View File
@@ -19,7 +19,8 @@ import { LicenceService } from '../services/licence.service'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class HomeComponent implements AfterContentInit { export class HomeComponent implements AfterContentInit {
public treeNodeLibraries: Array<any> | null = null public treeNodeLibraries: Array<any> | null = null
@@ -34,6 +34,8 @@
</p> </p>
</ng-container> </ng-container>
<p><strong>Protocol:</strong> {{ protocol }}</p>
<p> <p>
<strong>SYSSITE:</strong> <strong>SYSSITE:</strong>
<span <span
+186 -178
View File
@@ -1,178 +1,186 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core' import { Component, OnInit, ViewEncapsulation } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { AppService, LicenceService, SasService } from '../services' import { AppService, LicenceService, SasService } from '../services'
import { LicenseKeyData } from '../models/LicenseKeyData' import { LicenseKeyData } from '../models/LicenseKeyData'
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse' import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
enum LicenseActions { enum LicenseActions {
key = 'key', key = 'key',
register = 'register', register = 'register',
limit = 'limit', limit = 'limit',
update = 'update' update = 'update'
} }
@Component({ @Component({
selector: 'app-licensing', selector: 'app-licensing',
templateUrl: './licensing.component.html', templateUrl: './licensing.component.html',
styleUrls: ['./licensing.component.scss'], styleUrls: ['./licensing.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
}) standalone: false
export class LicensingComponent implements OnInit { })
public action: LicenseActions | null = null export class LicensingComponent implements OnInit {
public action: LicenseActions | null = null
public licenseErrors: { [key: string]: string } = {
missing: `Licence key is missing - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`, public licenseErrors: { [key: string]: string } = {
expired: `Licence key is expired - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`, missing: `Licence key is missing - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
invalid: `Licence key is invalid - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`, expired: `Licence key is expired - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
missmatch: `Your SYSSITE (below) is not found in the licence key - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.` invalid: `Licence key is invalid - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
} missmatch: `Your SYSSITE (below) is not found in the licence key - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`
}
public keyError: string | undefined
public errorDetails: string | undefined public keyError: string | undefined
public missmatchedKey: string | undefined public errorDetails: string | undefined
public licenceKeyValue: string = '' public missmatchedKey: string | undefined
public activationKeyValue: string = '' public licenceKeyValue: string = ''
public activationKeyValue: string = ''
public applyingKeys: boolean = false
public applyingKeys: boolean = false
public syssite = this.appService.syssite public protocol: string =
public currentLicenceKey = this.licenceService.licenceKey location.protocol === 'https:'
public currentActivationKey = this.licenceService.activationKey ? 'HTTPS - secure connection'
public isAppFreeTier = this.licenceService.isAppFreeTier : 'HTTP - insecure connection'
public userCountLimitation = this.licenceService.userCountLimitation
public syssite = this.appService.syssite
public licenseKeyData: LicenseKeyData | null = null public currentLicenceKey = this.licenceService.licenceKey
public currentActivationKey = this.licenceService.activationKey
public inputType: 'file' | 'paste' = 'file' public isAppFreeTier = this.licenceService.isAppFreeTier
public licenceFileError: string | undefined public userCountLimitation = this.licenceService.userCountLimitation
public licenceFileLoading: boolean = false
public licencefile: { filename: string } = { public licenseKeyData: LicenseKeyData | null = null
filename: ''
} public inputType: 'file' | 'paste' = 'file'
public licenceFileError: string | undefined
constructor( public licenceFileLoading: boolean = false
private route: ActivatedRoute, public licencefile: { filename: string } = {
private licenceService: LicenceService, filename: ''
private sasService: SasService, }
private appService: AppService
) {} constructor(
private route: ActivatedRoute,
ngOnInit(): void { private router: Router,
this.licenceKeyValue = this.currentLicenceKey || '' private licenceService: LicenceService,
this.activationKeyValue = this.currentActivationKey || '' private sasService: SasService,
private appService: AppService
this.route.queryParams.subscribe((queryParams: any) => { ) {}
this.keyError = queryParams.error
this.missmatchedKey = queryParams.missmatchId ngOnInit(): void {
this.licenceKeyValue = this.currentLicenceKey || ''
if (queryParams.details) { this.activationKeyValue = this.currentActivationKey || ''
this.errorDetails = atob(queryParams.details)
} this.route.queryParams.subscribe((queryParams: any) => {
}) this.keyError = queryParams.error
this.missmatchedKey = queryParams.missmatchId
this.route.params.subscribe((params: any) => {
let actionInUrl = params.action if (queryParams.details) {
this.errorDetails = atob(queryParams.details)
if (actionInUrl) { }
if (Object.values(LicenseActions).includes(actionInUrl)) { })
this.action = actionInUrl
} this.route.params.subscribe((params: any) => {
} let actionInUrl = params.action
})
if (actionInUrl) {
this.licenseKeyData = this.licenceService.getLicenseKeyData() if (Object.values(LicenseActions).includes(actionInUrl)) {
} this.action = actionInUrl
}
public trimKeys() { }
this.licenceKeyValue = this.licenceKeyValue.trim() })
this.activationKeyValue = this.activationKeyValue.trim()
} this.licenseKeyData = this.licenceService.getLicenseKeyData()
}
public copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[]) {
const syssiteString = syssite.join('\n') public trimKeys() {
this.licenceKeyValue = this.licenceKeyValue.trim()
navigator.clipboard.writeText(syssiteString).then(() => { this.activationKeyValue = this.activationKeyValue.trim()
copyIconRef.setAttribute('shape', 'check') }
copyIconRef.setAttribute('class', 'is-success')
copyTooltip.innerText = 'Copied!' public copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[]) {
const syssiteString = syssite.join('\n')
setTimeout(() => {
copyIconRef.setAttribute('shape', 'copy') navigator.clipboard.writeText(syssiteString).then(() => {
copyIconRef.removeAttribute('class') copyIconRef.setAttribute('shape', 'check')
copyTooltip.innerText = 'Copy to clipboard' copyIconRef.setAttribute('class', 'is-success')
}, 1000) copyTooltip.innerText = 'Copied!'
})
} setTimeout(() => {
copyIconRef.setAttribute('shape', 'copy')
public applyKeys() { copyIconRef.removeAttribute('class')
this.applyingKeys = true copyTooltip.innerText = 'Copy to clipboard'
}, 1000)
let table = { })
keyupload: [ }
{
ACTIVATION_KEY: this.activationKeyValue, public applyKeys() {
LICENCE_KEY: this.licenceKeyValue this.applyingKeys = true
}
] let table = {
} keyupload: [
{
this.sasService ACTIVATION_KEY: this.activationKeyValue,
.request('admin/registerkey', table) LICENCE_KEY: this.licenceKeyValue
.then((res: RequestWrapperResponse) => { }
if ( ]
res.adapterResponse.return && }
res.adapterResponse.return[0] &&
res.adapterResponse.return[0].MSG === 'SUCCESS' this.sasService
) { .request('admin/registerkey', table)
location.replace(location.href.split('#')[0]) .then((res: RequestWrapperResponse) => {
} if (
}) res.adapterResponse.return &&
.finally(() => { res.adapterResponse.return[0] &&
this.applyingKeys = false res.adapterResponse.return[0].MSG === 'SUCCESS'
}) ) {
} this.router.navigateByUrl('/').then(() => {
window.location.reload()
public onFileCapture(event: any, dropped = false) { })
let file = dropped ? event[0] : event.target.files[0] }
this.licencefile.filename = file.name })
.finally(() => {
if (!file) return this.applyingKeys = false
})
this.licenceFileLoading = true }
const reader = new FileReader() public onFileCapture(event: any, dropped = false) {
let file = dropped ? event[0] : event.target.files[0]
reader.onload = (evt) => { this.licencefile.filename = file.name
this.licenceFileError = 'Error reading file.'
if (!file) return
if (!evt || !evt.target) return
if (evt.target.readyState != 2) return this.licenceFileLoading = true
if (evt.target.error) return
if (!evt.target.result) return const reader = new FileReader()
this.licenceFileLoading = false reader.onload = (evt) => {
this.licenceFileError = undefined this.licenceFileError = 'Error reading file.'
const fileArr = evt.target.result.toString().split('\n')
this.activationKeyValue = fileArr[1] if (!evt || !evt.target) return
this.licenceKeyValue = fileArr[0] if (evt.target.readyState != 2) return
} if (evt.target.error) return
if (!evt.target.result) return
reader.readAsText(file)
} this.licenceFileLoading = false
this.licenceFileError = undefined
public switchType(type: 'paste' | 'file') { const fileArr = evt.target.result.toString().split('\n')
this.inputType = type this.activationKeyValue = fileArr[1]
} this.licenceKeyValue = fileArr[0]
}
get disableApplyButton(): boolean {
if (this.licenceKeyValue.length < 1 || this.activationKeyValue.length < 1) reader.readAsText(file)
return true }
if (
this.licenceKeyValue === this.currentLicenceKey && public switchType(type: 'paste' | 'file') {
this.activationKeyValue === this.currentActivationKey this.inputType = type
) }
return true
get disableApplyButton(): boolean {
return false if (this.licenceKeyValue.length < 1 || this.activationKeyValue.length < 1)
} return true
} if (
this.licenceKeyValue === this.currentLicenceKey &&
this.activationKeyValue === this.currentActivationKey
)
return true
return false
}
}
+2 -14
View File
@@ -239,13 +239,7 @@
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen> <clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div (click)="downloadSVG()" clrDropdownItem>SVG</div> <div (click)="downloadSVG()" clrDropdownItem>SVG</div>
<div <div (click)="downloadPNG()" clrDropdownItem>PNG</div>
*ngIf="!helperService.isMicrosoft"
(click)="downloadPNG()"
clrDropdownItem
>
PNG
</div>
<div (click)="downloadDot()" clrDropdownItem>Dot</div> <div (click)="downloadDot()" clrDropdownItem>Dot</div>
<div *ngIf="flatdata" (click)="downloadCSV()" clrDropdownItem> <div *ngIf="flatdata" (click)="downloadCSV()" clrDropdownItem>
CSV CSV
@@ -366,13 +360,7 @@
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen> <clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div (click)="renderToDownload('SVG')" clrDropdownItem>SVG</div> <div (click)="renderToDownload('SVG')" clrDropdownItem>SVG</div>
<div <div (click)="renderToDownload('PNG')" clrDropdownItem>PNG</div>
*ngIf="!helperService.isMicrosoft"
(click)="renderToDownload('PNG')"
clrDropdownItem
>
PNG
</div>
<div (click)="downloadDot(); cancelRenderingGraph()" clrDropdownItem> <div (click)="downloadDot(); cancelRenderingGraph()" clrDropdownItem>
Dot Dot
</div> </div>
+15 -46
View File
@@ -19,7 +19,8 @@ const moment = require('moment')
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class LineageComponent { export class LineageComponent {
public switchFlag: boolean = false public switchFlag: boolean = false
@@ -746,28 +747,13 @@ export class LineageComponent {
return URL.createObjectURL(svg_blob) return URL.createObjectURL(svg_blob)
} }
private getSVGBlob() {
let svg: any = document.getElementById('graph')
let serializer = new XMLSerializer()
let svg_blob = new Blob([serializer.serializeToString(svg)], {
type: 'image/svg+xml'
})
return svg_blob
}
downloadSVG() { downloadSVG() {
d3Viz.graphviz('#graph').resetZoom() d3Viz.graphviz('#graph').resetZoom()
if (navigator.appVersion.toString().indexOf('.NET') > 0) { let downloadLink = document.createElement('a')
window.navigator.msSaveBlob(this.getSVGBlob(), this.constructName('svg')) downloadLink.href = this.getSVGURL()
} else { downloadLink.download = this.constructName('svg')
let downloadLink = document.createElement('a') downloadLink.click()
downloadLink.href = this.getSVGURL()
downloadLink.download = this.constructName('svg')
document.body.appendChild(downloadLink)
downloadLink.click()
document.body.removeChild(downloadLink)
}
} }
async downloadPNG() { async downloadPNG() {
@@ -795,16 +781,11 @@ export class LineageComponent {
var a = document.createElement('a') var a = document.createElement('a')
var blob = new Blob([csvArray], { type: 'text/csv' }) var blob = new Blob([csvArray], { type: 'text/csv' })
if (navigator.appVersion.toString().indexOf('.NET') > 0) { var url = window.URL.createObjectURL(blob)
window.navigator.msSaveBlob(blob, this.constructName('csv')) a.href = url
} else { a.download = this.constructName('csv')
var url = window.URL.createObjectURL(blob) a.click()
a.href = url window.URL.revokeObjectURL(url)
a.download = this.constructName('csv')
a.click()
window.URL.revokeObjectURL(url)
a.remove()
}
} }
private getDotUrl() { private getDotUrl() {
@@ -813,23 +794,11 @@ export class LineageComponent {
return window.URL.createObjectURL(dot_blob) return window.URL.createObjectURL(dot_blob)
} }
private getDotBlob() {
let data = this.vizInput
let dot_blob = new Blob([data], { type: 'text/plain' })
return dot_blob
}
downloadDot() { downloadDot() {
if (navigator.appVersion.toString().indexOf('.NET') > 0) { let downloadLink = document.createElement('a')
window.navigator.msSaveBlob(this.getDotBlob(), this.constructName('txt')) downloadLink.href = this.getDotUrl()
} else { downloadLink.download = this.constructName('txt')
let downloadLink = document.createElement('a') downloadLink.click()
downloadLink.href = this.getDotUrl()
downloadLink.download = this.constructName('txt')
document.body.appendChild(downloadLink)
downloadLink.click()
document.body.removeChild(downloadLink)
}
} }
public showSvg() { public showSvg() {
@@ -51,7 +51,8 @@ class ValueFilter implements ClrDatagridStringFilterInterface<MetaData> {
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class MetadataComponent implements OnInit { export class MetadataComponent implements OnInit {
metaDataList: Array<any> | undefined metaDataList: Array<any> | undefined
@@ -6,4 +6,5 @@ export interface CellValidationSource {
extended_values?: string[] extended_values?: string[]
hash: string hash: string
count: number count: number
pending?: Promise<void>
} }
@@ -48,7 +48,8 @@ enum FileLoadingState {
selector: 'app-multi-dataset', selector: 'app-multi-dataset',
templateUrl: './multi-dataset.component.html', templateUrl: './multi-dataset.component.html',
styleUrls: ['./multi-dataset.component.scss'], styleUrls: ['./multi-dataset.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class MultiDatasetComponent implements OnInit, AfterViewInit { export class MultiDatasetComponent implements OnInit, AfterViewInit {
@HostBinding('class.content-container') contentContainerClass = true @HostBinding('class.content-container') contentContainerClass = true
@@ -159,14 +160,16 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
filters: true, filters: true,
stretchH: 'all', stretchH: 'all',
afterGetColHeader: baseAfterGetColHeader, afterGetColHeader: baseAfterGetColHeader,
modifyColWidth: this.maxWidthCheker modifyColWidth: this.maxWidthCheker,
theme: 'ht-theme-classic'
} }
// Exclude data from settings for HOT v16 - it will be loaded manually // Exclude data from settings for HOT v16 - it will be loaded manually
const { data, ...settingsWithoutData } = this.hotUserDatasets const { data, ...settingsWithoutData } = this.hotUserDatasets
this.hotUserDatasetsSettings = { this.hotUserDatasetsSettings = {
...settingsWithoutData, ...settingsWithoutData,
licenseKey: this.hotTableLicenseKey licenseKey: this.hotTableLicenseKey,
theme: 'ht-theme-classic'
} }
} }
@@ -7,7 +7,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class NotFoundComponent implements OnInit { export class NotFoundComponent implements OnInit {
constructor() {} constructor() {}
+2 -1
View File
@@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'
import { bytesToSize } from '@sasjs/utils/utils/bytesToSize' import { bytesToSize } from '@sasjs/utils/utils/bytesToSize'
@Pipe({ @Pipe({
name: 'convertSize' name: 'convertSize',
standalone: false
}) })
export class ConvertSizePipe implements PipeTransform { export class ConvertSizePipe implements PipeTransform {
transform(bytes: string | number, ...args: string[]): string { transform(bytes: string | number, ...args: string[]): string {
@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import * as moment from 'moment' import moment from 'moment'
@Pipe({ @Pipe({
name: 'dateTimeFormatter' name: 'dateTimeFormatter',
standalone: false
}) })
export class DateTimeFormatterPipe implements PipeTransform { export class DateTimeFormatterPipe implements PipeTransform {
transform(value: Date | string, type: string): string { transform(value: Date | string, type: string): string {
+2 -1
View File
@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
@Pipe({ @Pipe({
name: 'linkinze' name: 'linkinze',
standalone: false
}) })
export class LinkinzePipe implements PipeTransform { export class LinkinzePipe implements PipeTransform {
/** /**
+2 -1
View File
@@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'
import { HelperService } from '../services/helper.service' import { HelperService } from '../services/helper.service'
@Pipe({ @Pipe({
name: 'sasToJsDate' name: 'sasToJsDate',
standalone: false
}) })
export class sasToJsDatePipe implements PipeTransform { export class sasToJsDatePipe implements PipeTransform {
constructor(private helperService: HelperService) {} constructor(private helperService: HelperService) {}
@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
@Pipe({ @Pipe({
name: 'pkSpaceSeparate' name: 'pkSpaceSeparate',
standalone: false
}) })
export class PkSpaceSeparatePipe implements PipeTransform { export class PkSpaceSeparatePipe implements PipeTransform {
transform(value: string): string { transform(value: string): string {
+2 -1
View File
@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
@Pipe({ @Pipe({
name: 'prettyjson' name: 'prettyjson',
standalone: false
}) })
export class PrettyjsonPipe implements PipeTransform { export class PrettyjsonPipe implements PipeTransform {
transform(rawJson: any): string { transform(rawJson: any): string {
+2 -1
View File
@@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'
import { HelperService } from '../services/helper.service' import { HelperService } from '../services/helper.service'
@Pipe({ @Pipe({
name: 'secondsParser' name: 'secondsParser',
standalone: false
}) })
export class SecondsParserPipe implements PipeTransform { export class SecondsParserPipe implements PipeTransform {
constructor(private helperService: HelperService) {} constructor(private helperService: HelperService) {}
@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
@Pipe({ @Pipe({
name: 'thousandSeparator' name: 'thousandSeparator',
standalone: false
}) })
export class ThousandSeparatorPipe implements PipeTransform { export class ThousandSeparatorPipe implements PipeTransform {
transform(value: string | number, separator?: string): string { transform(value: string | number, separator?: string): string {
+2 -1
View File
@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
@Pipe({ @Pipe({
name: 'toNumber' name: 'toNumber',
standalone: false
}) })
export class ToNumberPipe implements PipeTransform { export class ToNumberPipe implements PipeTransform {
transform(value: string | number): number { transform(value: string | number): number {
+2 -1
View File
@@ -29,7 +29,8 @@ registerLocaleData(localeEnGB)
templateUrl: './query.component.html', templateUrl: './query.component.html',
styleUrls: ['./query.component.scss'], styleUrls: ['./query.component.scss'],
providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }], providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class QueryComponent export class QueryComponent
implements AfterViewInit, AfterContentInit, OnDestroy implements AfterViewInit, AfterContentInit, OnDestroy
@@ -1,4 +1,5 @@
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { sanitiseForSas } from '../../shared/utils/sanitise'
import { SasStoreService } from '../../services/sas-store.service' import { SasStoreService } from '../../services/sas-store.service'
import { import {
Component, Component,
@@ -30,7 +31,8 @@ interface ChangesObj {
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ApproveDetailsComponent implements AfterViewInit, OnDestroy { export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
private _detailsSub: Subscription | undefined private _detailsSub: Subscription | undefined
@@ -135,7 +137,7 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
public async rejecting() { public async rejecting() {
this.rejectLoading = true this.rejectLoading = true
this.submitReason = this.submitReason.replace(/\n/g, '. ') this.submitReason = sanitiseForSas(this.submitReason.replace(/\n/g, '. '))
let rejParams = { let rejParams = {
STP_ACTION: 'REJECT_TABLE', STP_ACTION: 'REJECT_TABLE',
@@ -38,9 +38,7 @@ class SubmittedFilter implements ClrDatagridStringFilterInterface<ApproveData> {
} }
} }
class SubmitReasonFilter class SubmitReasonFilter implements ClrDatagridStringFilterInterface<ApproveData> {
implements ClrDatagridStringFilterInterface<ApproveData>
{
accepts(data: ApproveData, search: string): boolean { accepts(data: ApproveData, search: string): boolean {
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0 return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
} }
@@ -53,7 +51,8 @@ class SubmitReasonFilter
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ApproveComponent implements OnInit { export class ApproveComponent implements OnInit {
public approveList: Array<ApproveData> | undefined public approveList: Array<ApproveData> | undefined
@@ -38,9 +38,7 @@ class SubmitterFilter implements ClrDatagridStringFilterInterface<HistoryData> {
} }
} }
class SubmitReasonFilter class SubmitReasonFilter implements ClrDatagridStringFilterInterface<HistoryData> {
implements ClrDatagridStringFilterInterface<HistoryData>
{
accepts(data: HistoryData, search: string): boolean { accepts(data: HistoryData, search: string): boolean {
return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0 return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
} }
@@ -65,7 +63,8 @@ class ReviewedFilter implements ClrDatagridStringFilterInterface<HistoryData> {
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class HistoryComponent implements OnInit { export class HistoryComponent implements OnInit {
public history: Array<any> = [] public history: Array<any> = []
@@ -17,17 +17,13 @@ interface SubmitterData {
approver: string approver: string
} }
class SubmittedFilter class SubmittedFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
implements ClrDatagridStringFilterInterface<SubmitterData>
{
accepts(data: SubmitterData, search: string): boolean { accepts(data: SubmitterData, search: string): boolean {
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0 return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
} }
} }
class SubmitReasonFilter class SubmitReasonFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
implements ClrDatagridStringFilterInterface<SubmitterData>
{
accepts(data: SubmitterData, search: string): boolean { accepts(data: SubmitterData, search: string): boolean {
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0 return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
} }
@@ -40,7 +36,8 @@ class SubmitReasonFilter
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class SubmitterComponent implements OnInit, AfterViewInit { export class SubmitterComponent implements OnInit, AfterViewInit {
public remained: number = 0 public remained: number = 0
+2 -1
View File
@@ -13,7 +13,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class RoleComponent implements OnInit { export class RoleComponent implements OnInit {
public roles: Array<any> | undefined public roles: Array<any> | undefined
@@ -7,7 +7,8 @@ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class HomeRouteComponent implements OnInit, OnDestroy { export class HomeRouteComponent implements OnInit, OnDestroy {
constructor() {} constructor() {}
@@ -5,7 +5,8 @@ import { Component, OnInit, OnDestroy } from '@angular/core'
templateUrl: './multi-dataset-route.component.html', templateUrl: './multi-dataset-route.component.html',
host: { host: {
class: 'content-container' class: 'content-container'
} },
standalone: false
}) })
export class MultiDatasetRouteComponent implements OnInit, OnDestroy { export class MultiDatasetRouteComponent implements OnInit, OnDestroy {
constructor() {} constructor() {}
@@ -7,7 +7,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ReviewRouteComponent implements OnInit { export class ReviewRouteComponent implements OnInit {
constructor() {} constructor() {}
@@ -7,7 +7,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class UsernavRouteComponent implements OnInit { export class UsernavRouteComponent implements OnInit {
constructor() {} constructor() {}
@@ -7,7 +7,8 @@ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ViewRouteComponent implements OnInit, OnDestroy { export class ViewRouteComponent implements OnInit, OnDestroy {
constructor() {} constructor() {}
@@ -7,7 +7,8 @@ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class XLMapRouteComponent implements OnInit, OnDestroy { export class XLMapRouteComponent implements OnInit, OnDestroy {
constructor() {} constructor() {}
+2 -32
View File
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import cloneDeep from 'lodash-es/cloneDeep' import cloneDeep from 'lodash-es/cloneDeep'
import * as CryptoMD5 from 'crypto-js/md5' import CryptoMD5 from 'crypto-js/md5'
import { SasService } from './sas.service' import { SasService } from './sas.service'
const librariesToShow = 50 const librariesToShow = 50
@@ -11,12 +11,8 @@ const librariesToShow = 50
export class HelperService { export class HelperService {
public shownLibraries: number = librariesToShow public shownLibraries: number = librariesToShow
public loadMoreCount: number = librariesToShow public loadMoreCount: number = librariesToShow
public isMicrosoft: boolean = false
constructor(private sasService: SasService) { constructor(private sasService: SasService) {}
this.isMicrosoft = this.isIEorEDGE()
console.log('Is IE or Edge?', this.isMicrosoft)
}
/** /**
* Converts a JavaScript date object to a SAS Date or Datetime, given the logic below: * Converts a JavaScript date object to a SAS Date or Datetime, given the logic below:
@@ -215,32 +211,6 @@ export class HelperService {
}) })
} }
public isIEorEDGE() {
var ua = window.navigator.userAgent
var msie = ua.indexOf('MSIE ')
if (msie > 0) {
// IE 10 or older => return version number
return true
}
var trident = ua.indexOf('Trident/')
if (trident > 0) {
// IE 11 => return version number
var rv = ua.indexOf('rv:')
return true
}
var edge = ua.indexOf('Edge/')
if (edge > 0) {
// Edge (IE 12+) => return version number
return true
}
// other browser
return false
}
public convertObjectsToArray( public convertObjectsToArray(
objectArray: Array<object>, objectArray: Array<object>,
deepClone: boolean = false deepClone: boolean = false
+1 -1
View File
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs' import { BehaviorSubject } from 'rxjs'
import { LicenseKeyData } from '../models/LicenseKeyData' import { LicenseKeyData } from '../models/LicenseKeyData'
import { SasService } from './sas.service' import { SasService } from './sas.service'
import * as moment from 'moment' import moment from 'moment'
import * as base64Converter from 'base64-arraybuffer' import * as base64Converter from 'base64-arraybuffer'
import * as encoding from 'text-encoding' import * as encoding from 'text-encoding'
import { Router } from '@angular/router' import { Router } from '@angular/router'
+13 -3
View File
@@ -155,13 +155,23 @@ export class SasStoreService {
.adapterResponse .adapterResponse
} }
private libsPromise: Promise<any> | null = null
/** /**
* *
* @returns All libraries * @returns All libraries
*/ */
public async viewLibs() { public viewLibs() {
return (await this.sasService.request('public/viewlibs', null)) if (!this.libsPromise) {
.adapterResponse 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) { public async refreshLibInfo(libref: string) {
+6 -3
View File
@@ -120,9 +120,12 @@ export class SasViyaService {
} }
getComputeContexts(): Observable<ViyaComputeContexts> { getComputeContexts(): Observable<ViyaComputeContexts> {
return this.get<ViyaComputeContexts>(`${this.serverUrl}/compute/contexts`, { return this.get<ViyaComputeContexts>(
withCredentials: true `${this.serverUrl}/compute/contexts?limit=1000`,
}) {
withCredentials: true
}
)
} }
getComputeContextById(id: string): Observable<ComputeContextDetails> { getComputeContextById(id: string): Observable<ComputeContextDetails> {
+17
View File
@@ -641,6 +641,23 @@ export class SasService {
this.sasjsAdapter.setDebugState(state) 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() { public getSasjsInstance() {
return this.sasjsAdapter return this.sasjsAdapter
} }
@@ -17,7 +17,8 @@ import { AbortDetails, InfoModal } from '../../models/InfoModal'
selector: 'app-info-modal', selector: 'app-info-modal',
templateUrl: './info-modal.component.html', templateUrl: './info-modal.component.html',
styleUrls: ['./info-modal.component.scss'], styleUrls: ['./info-modal.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class InfoModalComponent implements OnInit { export class InfoModalComponent implements OnInit {
@Output() onConfirmModalClick: EventEmitter<any> = new EventEmitter() @Output() onConfirmModalClick: EventEmitter<any> = new EventEmitter()
@@ -7,7 +7,8 @@ import { AlertsService } from './alerts.service'
selector: 'app-alerts', selector: 'app-alerts',
templateUrl: './alerts.component.html', templateUrl: './alerts.component.html',
styleUrls: ['./alerts.component.scss'], styleUrls: ['./alerts.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class AlertsComponent implements OnInit { export class AlertsComponent implements OnInit {
public alerts: Array<Alert> = [] public alerts: Array<Alert> = []
@@ -19,7 +19,8 @@ export type OnLoadingMoreEvent = {
selector: 'app-autocomplete', selector: 'app-autocomplete',
templateUrl: './autocomplete.component.html', templateUrl: './autocomplete.component.html',
styleUrls: ['./autocomplete.component.scss'], styleUrls: ['./autocomplete.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class AutocompleteComponent implements OnInit, AfterViewInit { export class AutocompleteComponent implements OnInit, AfterViewInit {
@ViewChild('input') inputElement: any @ViewChild('input') inputElement: any
@@ -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)
}
}
@@ -4,7 +4,8 @@ import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'
selector: 'contact-link', selector: 'contact-link',
templateUrl: './contact-link.component.html', templateUrl: './contact-link.component.html',
styleUrls: ['./contact-link.component.scss'], styleUrls: ['./contact-link.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ContactLinkComponent implements OnInit { export class ContactLinkComponent implements OnInit {
@Input() classes: string = '' @Input() classes: string = ''
@@ -15,7 +15,8 @@ import { Tab } from './models/dsmeta-groupped.model'
selector: 'app-dataset-info', selector: 'app-dataset-info',
templateUrl: './dataset-info.component.html', templateUrl: './dataset-info.component.html',
styleUrls: ['./dataset-info.component.scss'], styleUrls: ['./dataset-info.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class DatasetInfoComponent implements OnInit, OnChanges { export class DatasetInfoComponent implements OnInit, OnChanges {
@Input() open: boolean = false @Input() open: boolean = false
@@ -22,7 +22,8 @@ import { TableClickEmitter } from './models/TableClickEmitter'
selector: 'dc-tree', selector: 'dc-tree',
templateUrl: './dc-tree.component.html', templateUrl: './dc-tree.component.html',
styleUrls: ['./dc-tree.component.scss'], styleUrls: ['./dc-tree.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class DcTreeComponent implements OnInit, AfterViewInit, OnChanges { export class DcTreeComponent implements OnInit, AfterViewInit, OnChanges {
// REFACTOR NOTICE // REFACTOR NOTICE
@@ -15,12 +15,14 @@ import {
} from './models/dc-validation.model' } from './models/dc-validation.model'
import { DQRule, DQRuleTypes } from './models/dq-rules.model' import { DQRule, DQRuleTypes } from './models/dq-rules.model'
import { getDqDataCols } from './utils/getDqDataCols' import { getDqDataCols } from './utils/getDqDataCols'
import { getNotNullDefault } from './utils/getNotNullDefault'
import { mergeColsRules } from './utils/mergeColsRules' import { mergeColsRules } from './utils/mergeColsRules'
import { parseColType } from './utils/parseColType' import { parseColType } from './utils/parseColType'
import { dqValidate } from './validations/dq-validation' import { dqValidate } from './validations/dq-validation'
import { specialMissingNumericValidator } from './validations/hot-custom-validators' import { specialMissingNumericValidator } from './validations/hot-custom-validators'
import { applyNumericFormats } from './utils/applyNumericFormats' import { applyNumericFormats } from './utils/applyNumericFormats'
import { CustomAutocompleteEditor } from './editors/numericAutocomplete' import { CustomAutocompleteEditor } from './editors/numericAutocomplete'
import { makeNumberFormatRenderer } from '../../editor/utils/renderers.utils'
export class DcValidator { export class DcValidator {
private rules: DcValidation[] = [] private rules: DcValidation[] = []
@@ -133,6 +135,38 @@ export class DcValidator {
} }
} }
/**
* Returns the RULE_VALUE for a NOTNULL rule on the given column.
* Used for auto-populating default values when cells are empty.
* Converts to number for numeric columns.
*
* @param col column name
* @returns RULE_VALUE (string or number) if NOTNULL rule exists, otherwise undefined
*/
getNotNullDefaultValue(col: string): string | number | undefined {
const colRule = this.getRule(col)
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 * Retrieves dropdown source for given dc validation rule
* The values comes from MPE_SELECTBOX table * The values comes from MPE_SELECTBOX table
@@ -270,10 +304,18 @@ export class DcValidator {
) )
if (source.length > 0) { 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].source = source
this.rules[i].type = 'autocomplete'
this.rules[i].editor = 'autocomplete.custom' this.rules[i].editor = 'autocomplete.custom'
this.rules[i].renderer = 'autocomplete'
this.rules[i].filter = false this.rules[i].filter = false
if (this.rules[i].sasType !== 'num') {
this.rules[i].type = 'autocomplete'
}
} }
if (this.hasDqRules(ruleColName, ['SOFTSELECT'])) { if (this.hasDqRules(ruleColName, ['SOFTSELECT'])) {
@@ -295,6 +337,30 @@ export class DcValidator {
if (this.hasDqRules(ruleColName, ['NOTNULL'])) { if (this.hasDqRules(ruleColName, ['NOTNULL'])) {
this.rules[i].allowEmpty = false 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. // Correct format comes as STRING from SAS. That could be also fixed on SAS side.
@@ -1,8 +1,9 @@
import Handsontable from 'handsontable' import Handsontable from 'handsontable'
import Core from 'handsontable/core' import Core from 'handsontable/core'
export class CustomAutocompleteEditor extends Handsontable.editors export class CustomAutocompleteEditor
.AutocompleteEditor { extends Handsontable.editors.AutocompleteEditor
{
constructor(instance: Core) { constructor(instance: Core) {
super(instance) super(instance)
} }
@@ -10,12 +10,14 @@ export interface DcColumnSettings {
valid?: boolean valid?: boolean
desc?: string desc?: string
clsRule?: 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 {} export interface DcValidation extends HotColumnSettings, DcColumnSettings {}
export interface DcValidationRuleUpdate export interface DcValidationRuleUpdate
extends Handsontable.ColumnSettings, extends Handsontable.ColumnSettings, DcColumnSettings {
DcColumnSettings {
data?: string data?: string
} }
@@ -14,3 +14,7 @@ export type DQRuleTypes =
| 'CASE' | 'CASE'
| 'MINVAL' | 'MINVAL'
| 'MAXVAL' | '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 = [ const example_dqData = [
@@ -313,6 +344,30 @@ const example_dqRules: any = [
RULE_TYPE: 'CASE', RULE_TYPE: 'CASE',
RULE_VALUE: 'LOWCASE', RULE_VALUE: 'LOWCASE',
X: 0 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()
})
})
@@ -1,3 +1,4 @@
import { DQRule } from '../models/dq-rules.model'
import { getHotDataSchema } from '../utils/getHotDataSchema' import { getHotDataSchema } from '../utils/getHotDataSchema'
describe('DC Validator - hot data schema', () => { describe('DC Validator - hot data schema', () => {
@@ -8,4 +9,58 @@ describe('DC Validator - hot data schema', () => {
).toEqual(1) ).toEqual(1)
expect(getHotDataSchema('missing')).toEqual('') expect(getHotDataSchema('missing')).toEqual('')
}) })
describe('NOTNULL defaults', () => {
const dqRules: DQRule[] = [
{
BASE_COL: 'TEXT_COL',
RULE_TYPE: 'NOTNULL',
RULE_VALUE: 'default_text',
X: 1
},
{ BASE_COL: 'NUM_COL', RULE_TYPE: 'NOTNULL', RULE_VALUE: '42', X: 1 }
]
it('should return NOTNULL default for text column', () => {
expect(getHotDataSchema('text', { data: 'TEXT_COL' }, dqRules)).toEqual(
'default_text'
)
})
it('should return NOTNULL default as number for numeric column', () => {
expect(getHotDataSchema('numeric', { data: 'NUM_COL' }, dqRules)).toEqual(
42
)
})
it('should fall back to type default when no NOTNULL rule exists', () => {
expect(
getHotDataSchema('numeric', { data: 'OTHER_COL' }, dqRules)
).toEqual('')
})
it('should prioritize NOTNULL over autocomplete first option', () => {
const rulesWithAutocomplete: DQRule[] = [
{
BASE_COL: 'SELECT_COL',
RULE_TYPE: 'NOTNULL',
RULE_VALUE: 'priority_value',
X: 1
},
{
BASE_COL: 'SELECT_COL',
RULE_TYPE: 'HARDSELECT',
RULE_VALUE: 'ignored',
X: 1
}
]
expect(
getHotDataSchema(
'autocomplete',
{ data: 'SELECT_COL', source: ['first', 'second'] },
rulesWithAutocomplete
)
).toEqual('priority_value')
})
})
}) })
@@ -0,0 +1,65 @@
import { DQRule } from '../models/dq-rules.model'
import { getNotNullDefault } from '../utils/getNotNullDefault'
describe('DC Validator - getNotNullDefault', () => {
const dqRules: DQRule[] = [
{
BASE_COL: 'TEXT_COL',
RULE_TYPE: 'NOTNULL',
RULE_VALUE: 'default_text',
X: 1
},
{ BASE_COL: 'NUM_COL', RULE_TYPE: 'NOTNULL', RULE_VALUE: '42', X: 1 },
{ BASE_COL: 'EMPTY_RULE', RULE_TYPE: 'NOTNULL', RULE_VALUE: ' ', X: 1 },
{
BASE_COL: 'OTHER_COL',
RULE_TYPE: 'HARDSELECT',
RULE_VALUE: 'some_value',
X: 1
}
]
it('should return string value for text columns', () => {
expect(getNotNullDefault('TEXT_COL', dqRules, 'text')).toEqual(
'default_text'
)
})
it('should return number for numeric columns when RULE_VALUE is numeric', () => {
expect(getNotNullDefault('NUM_COL', dqRules, 'numeric')).toEqual(42)
})
it('should return string for numeric columns when RULE_VALUE is not numeric', () => {
const rulesWithNonNumeric: DQRule[] = [
{
BASE_COL: 'NUM_COL',
RULE_TYPE: 'NOTNULL',
RULE_VALUE: 'not_a_number',
X: 1
}
]
expect(
getNotNullDefault('NUM_COL', rulesWithNonNumeric, 'numeric')
).toEqual('not_a_number')
})
it('should return undefined for empty RULE_VALUE', () => {
expect(getNotNullDefault('EMPTY_RULE', dqRules, 'text')).toBeUndefined()
})
it('should return undefined for columns without NOTNULL rule', () => {
expect(getNotNullDefault('OTHER_COL', dqRules, 'text')).toBeUndefined()
})
it('should return undefined for non-existent columns', () => {
expect(getNotNullDefault('MISSING_COL', dqRules, 'text')).toBeUndefined()
})
it('should return undefined for empty dqRules array', () => {
expect(getNotNullDefault('TEXT_COL', [], 'text')).toBeUndefined()
})
it('should return string when colType is undefined', () => {
expect(getNotNullDefault('NUM_COL', dqRules, undefined)).toEqual('42')
})
})
@@ -0,0 +1,39 @@
import { isEmpty } from '../utils/isEmpty'
describe('DC Validator - isEmpty', () => {
it('should return true for null', () => {
expect(isEmpty(null)).toBe(true)
})
it('should return true for undefined', () => {
expect(isEmpty(undefined)).toBe(true)
})
it('should return true for empty string', () => {
expect(isEmpty('')).toBe(true)
})
it('should return true for whitespace-only string', () => {
expect(isEmpty(' ')).toBe(true)
expect(isEmpty('\t\n')).toBe(true)
})
it('should return false for non-empty string', () => {
expect(isEmpty('hello')).toBe(false)
expect(isEmpty(' hello ')).toBe(false)
})
it('should return false for number zero', () => {
expect(isEmpty(0)).toBe(false)
})
it('should return false for non-zero numbers', () => {
expect(isEmpty(42)).toBe(false)
expect(isEmpty(-1)).toBe(false)
})
it('should return false for boolean values', () => {
expect(isEmpty(true)).toBe(false)
expect(isEmpty(false)).toBe(false)
})
})
@@ -38,11 +38,52 @@ describe('DC Validator - merge spec rules', () => {
data: 'test_col', data: 'test_col',
desc: 'test_desc', desc: 'test_desc',
clsRule: 'cls_rule', clsRule: 'cls_rule',
length: 8 length: 8,
sasType: 'test_type'
} }
] ]
expect(mergeColsRules(cols, rules, $dataFormats)).toEqual(expected) expect(mergeColsRules(cols, rules, $dataFormats)).toEqual(expected)
expect(cols[0].TYPE).toEqual('test_type') 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
}

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