Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc34aca00 | ||
| f97ac70678 | |||
| 6ceb681463 | |||
| 716ee6eba0 | |||
| f6b0f6b0cd | |||
| 737a652ff0 | |||
| 2995e5c9dc | |||
| 338c7a2e41 | |||
| ad27358deb | |||
| 495754816c | |||
|
|
96f2518af9 | ||
|
|
280bdeeb1b | ||
| 46cdeb0bab | |||
| d41f88f8bf | |||
| 815d6e97a8 | |||
| 4e35aefe41 | |||
| ca84915e43 | |||
|
|
31cc7e9e4d | ||
| 4ec107705e | |||
|
|
7740d2ac86 | ||
|
|
8c2aeacc85 | ||
|
|
8b8e8aec15 | ||
|
|
6ac3f660e9 | ||
| 7ee576a9c1 | |||
| f4c8699aaf | |||
|
|
4273ca6e5c | ||
|
|
4ba043b77e | ||
| 0169415ea2 | |||
| 86791dbaca | |||
| d5b58a3cbd | |||
| 3d8281d27e | |||
| b1a014c7bc | |||
| 505d0af2b3 | |||
| ece6bd1d78 | |||
| 8dc18b155a | |||
| aecd597687 | |||
| c99f106bae | |||
| d2fc7ae6fe | |||
|
|
c1c1d0055a | ||
| f37ec82d39 | |||
| 6e9e30e0f0 | |||
| 990ddb5cd3 | |||
| c6ebbb48bb | |||
| 95cc0b1c91 | |||
| 81b282f1f1 | |||
| 8c5b357dd2 | |||
| a13b2cbfd2 | |||
| 19617c2285 | |||
| f9794a973f | |||
| 65efe62d19 | |||
| 683ddcaf53 | |||
|
|
113e0bbc3c | ||
| 2af97e40bf | |||
|
|
83cbe3aece | ||
| ceac1ba614 | |||
|
|
765fdbdf9d | ||
|
|
43ae73c5f3 | ||
|
|
e57a0de8a9 | ||
|
|
3de491105b | ||
|
|
2fe690e962 | ||
|
|
b826d37086 | ||
|
|
bfbfd55fe7 | ||
|
|
15f38efd52 | ||
|
|
5d25681485 | ||
|
|
d26df376f8 | ||
| cff3fb3bad | |||
|
|
fb3c49aa8b | ||
|
|
af1657e226 | ||
|
|
d7c7302c12 | ||
| 26ce95f7c1 | |||
|
|
4924df2ef3 | ||
| 2e141a5d52 | |||
|
|
cb1978bcaf | ||
|
|
387f5122f1 | ||
|
|
db5887de21 | ||
| fe24d9bcbd | |||
|
|
6c6b1cbf46 | ||
|
|
4d65c9c999 | ||
| 4417279275 | |||
|
|
365f12996d | ||
|
|
ef1015f33b | ||
| b43dfb5cf4 | |||
|
|
225e693d1f | ||
|
|
fda91770be | ||
| d512876e0b | |||
|
|
2ba4b5383e | ||
| ecc3184609 | |||
|
|
712b384848 | ||
|
|
26cdd73331 | ||
|
|
919aa6dcfe | ||
|
|
3bb3093b49 | ||
|
|
9c12250558 | ||
|
|
6c843f64fb | ||
|
|
3fda7dc5b0 | ||
|
|
22ec7f0340 | ||
|
|
378461dcbb | ||
|
|
905c7b9d3c | ||
|
|
670ec2c71c | ||
|
|
b419cd5078 | ||
|
|
b1db4ea590 | ||
| 822ddb1274 | |||
|
|
672dd6d4f1 | ||
|
|
b3ac73d903 | ||
|
|
f8554dd5e7 | ||
| 88679c0c9a | |||
|
|
a08a717ca8 | ||
| c8b6fdbfdb | |||
|
|
b495c41626 | ||
|
|
7f4be474c6 | ||
| 7f6f68fcbb | |||
|
|
03fd7db033 | ||
|
|
9dc5c66f7b | ||
|
|
aa1b08632e | ||
|
|
6bbe354c9e | ||
|
|
8ff429793b | ||
|
|
70d010127a | ||
|
|
696717c509 | ||
|
|
71c308d052 | ||
|
|
bed5b320ad | ||
| b0e827412e | |||
|
|
63e9af402e | ||
| fd55105f62 |
@@ -10,3 +10,14 @@ else
|
|||||||
echo "❌ Prettier check failed! We ran lint:fix for you. Please add & commit again."
|
echo "❌ Prettier check failed! We ran lint:fix for you. Please add & commit again."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
## Avoid large commits
|
||||||
|
# https://www.backblaze.com/blog/how-many-bytes-are-in-a-megabyte-really/
|
||||||
|
size_limit=$((2 * 2**20)) # 2mbs
|
||||||
|
# https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt---disk-usage
|
||||||
|
commit_size=$(git rev-list --disk-usage HEAD^..HEAD)
|
||||||
|
test "$commit_size" -lt "$size_limit" || (
|
||||||
|
echo "Commit size is too large: $commit_size > $size_limit"
|
||||||
|
echo "Force commit using --no-verify"
|
||||||
|
exit 1
|
||||||
|
)
|
||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.15.1
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Install Google Chrome
|
- name: Install Google Chrome
|
||||||
run: |
|
run: |
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.15.1
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
- run: apt install -y ./google-chrome*.deb;
|
- run: apt install -y ./google-chrome*.deb;
|
||||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
- run: export CHROME_BIN=/usr/bin/google-chrome
|
||||||
- run: apt-get update -y
|
- run: apt-get update -y
|
||||||
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
|
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb
|
||||||
- run: apt -y install jq
|
- run: apt -y install jq
|
||||||
|
|
||||||
- name: Write cypress credentials
|
- name: Write cypress credentials
|
||||||
@@ -126,7 +126,7 @@ 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
|
||||||
npm start & 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/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()
|
||||||
|
|||||||
101
.gitea/workflows/lighthouse.yaml
Normal file
101
.gitea/workflows/lighthouse.yaml
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
name: Lighthouse Checks
|
||||||
|
run-name: Running Lighthouse Performance and Accessibility Checks on Pull Request
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lighthouse:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [24.5.0]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Install Google Chrome
|
||||||
|
run: |
|
||||||
|
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
|
||||||
|
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y google-chrome-stable xvfb
|
||||||
|
|
||||||
|
- name: Install pm2 for process management
|
||||||
|
run: npm i -g pm2
|
||||||
|
|
||||||
|
- name: Install @sasjs/cli
|
||||||
|
run: npm i -g @sasjs/cli
|
||||||
|
|
||||||
|
- name: Install wait-on globally
|
||||||
|
run: npm install -g wait-on
|
||||||
|
|
||||||
|
- name: Create .env file for sasjs/server
|
||||||
|
run: |
|
||||||
|
touch .env
|
||||||
|
echo RUN_TIMES=js >> .env
|
||||||
|
echo NODE_PATH=node >> .env
|
||||||
|
echo CORS=enable >> .env
|
||||||
|
echo WHITELIST=http://localhost:4200 >> .env
|
||||||
|
cat .env
|
||||||
|
|
||||||
|
- name: Download sasjs/server package from github using curl
|
||||||
|
run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||||
|
|
||||||
|
- name: Unzip downloaded package
|
||||||
|
run: unzip linux.zip
|
||||||
|
|
||||||
|
- name: Run sasjs server
|
||||||
|
run: pm2 start api-linux --wait-ready
|
||||||
|
|
||||||
|
- name: Write .npmrc file
|
||||||
|
run: echo "$NPMRC" > client/.npmrc
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
NPMRC: ${{ secrets.NPMRC}}
|
||||||
|
|
||||||
|
- name: Install npm dependencies
|
||||||
|
run: |
|
||||||
|
cd client
|
||||||
|
# Decrypt and Install sheet
|
||||||
|
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
||||||
|
npm ci
|
||||||
|
npm install -g replace-in-files-cli
|
||||||
|
|
||||||
|
- name: Update appLoc in index.html
|
||||||
|
run: |
|
||||||
|
cd client
|
||||||
|
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/proj/sasjs/genesis-mocks"' ./src/index.html
|
||||||
|
|
||||||
|
- name: Build Frontend
|
||||||
|
run: |
|
||||||
|
cd client
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Deploy JS mocked services and frontend to the local SASjs Server instance
|
||||||
|
run: |
|
||||||
|
cd sas/mocks
|
||||||
|
npm ci
|
||||||
|
sasjs cbd -t server-ci
|
||||||
|
|
||||||
|
- name: Start frontend server
|
||||||
|
run: |
|
||||||
|
cd client
|
||||||
|
npx ng serve --host 0.0.0.0 --port 4200 &
|
||||||
|
wait-on http://localhost:4200
|
||||||
|
|
||||||
|
- name: Run Lighthouse CI
|
||||||
|
run: |
|
||||||
|
cd client
|
||||||
|
npx lhci autorun
|
||||||
|
|
||||||
|
- name: Lighthouse Result Artifacts
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: Lighthouse results
|
||||||
|
path: client/lighthouse-reports
|
||||||
|
include-hidden-files: true
|
||||||
@@ -13,12 +13,12 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.14.0
|
node-version: 24.5.0
|
||||||
|
|
||||||
- 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: |
|
||||||
@@ -41,7 +41,7 @@ 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 --audit-level=critical --omit=dev
|
||||||
cd ./sas
|
cd ./sas
|
||||||
@@ -68,19 +68,19 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.14.0
|
node-version: 24.5.0
|
||||||
|
|
||||||
- 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
|
- run: apt-get update
|
||||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||||
- run: apt install -y ./google-chrome*.deb;
|
- run: apt install -y ./google-chrome*.deb;
|
||||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
- run: export CHROME_BIN=/usr/bin/google-chrome
|
||||||
- run: apt-get update -y
|
- run: apt-get update -y
|
||||||
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
|
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb
|
||||||
- run: apt -y install jq
|
- run: apt -y install jq
|
||||||
|
|
||||||
- name: Write cypress credentials
|
- name: Write cypress credentials
|
||||||
@@ -136,7 +136,7 @@ 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
|
||||||
npm start & 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/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()
|
||||||
@@ -158,7 +158,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.14.0
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
@@ -228,6 +228,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
|
||||||
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||||
@@ -21,3 +22,4 @@ sasjsresults
|
|||||||
.sasjsrc
|
.sasjsrc
|
||||||
client/.npmrc
|
client/.npmrc
|
||||||
*~
|
*~
|
||||||
|
.lighthouseci
|
||||||
124
CHANGELOG.md
124
CHANGELOG.md
@@ -1,3 +1,127 @@
|
|||||||
|
# [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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* jsrsasign, @sasjs/cli bump ([365f129](https://git.datacontroller.io/dc/dc/commit/365f12996db3ef50a4f4f099d5af15696c43bb42))
|
||||||
|
|
||||||
|
## [7.2.1](https://git.datacontroller.io/dc/dc/compare/v7.2.0...v7.2.1) (2025-08-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removing localhost from index.html ([225e693](https://git.datacontroller.io/dc/dc/commit/225e693d1fd4381f2b8ce42fecb508f0a9e9dad8))
|
||||||
|
|
||||||
|
# [7.2.0](https://git.datacontroller.io/dc/dc/compare/v7.1.1...v7.2.0) (2025-08-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **ci:** cypress dependency package not available anymore ([26cdd73](https://git.datacontroller.io/dc/dc/commit/26cdd733315ef8babe9498ce93f6eb29c587dabd))
|
||||||
|
* **hot v16 migration:** multi dataset fixed issues, and cypress tests adapted ([712b384](https://git.datacontroller.io/dc/dc/commit/712b3848480a8769d149e00b0d2de91396022b66))
|
||||||
|
* obsolete cypress deps ([2ba4b53](https://git.datacontroller.io/dc/dc/commit/2ba4b5383e23bff8dfeb82b0ef473e5871c94709))
|
||||||
|
* remaining hot migrations - handsontable/angular-wrapper ([b419cd5](https://git.datacontroller.io/dc/dc/commit/b419cd507837e846e9dfcc6b729254d56cc196e6))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* lighthouse accessibility check pipeline ([670ec2c](https://git.datacontroller.io/dc/dc/commit/670ec2c71cb2d24e9d79e297a8cbc6136aa315c8))
|
||||||
|
|
||||||
|
## [7.1.1](https://git.datacontroller.io/dc/dc/compare/v7.1.0...v7.1.1) (2025-07-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **viewboxes:** hot v16 fails to load because of relative height `100%` ([672dd6d](https://git.datacontroller.io/dc/dc/commit/672dd6d4f1fda27e3706dd7caa42b45922319497))
|
||||||
|
|
||||||
|
# [7.1.0](https://git.datacontroller.io/dc/dc/compare/v7.0.3...v7.1.0) (2025-07-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adapter bump ([b495c41](https://git.datacontroller.io/dc/dc/commit/b495c41626c85b7c4141d9361e4d3a826efd6c05))
|
||||||
|
* bumping CLI to 4.12.10 ([a08a717](https://git.datacontroller.io/dc/dc/commit/a08a717ca8d49e8a7d63f3fd91c6a7d42a1d6d8b))
|
||||||
|
* bumping sasjs/core and sasjs/cli ([63e9af4](https://git.datacontroller.io/dc/dc/commit/63e9af402ed65f6be4426e76ee1376a40e6ed097))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* improving accessibility score up to 100, hot update to v16.0.1 ([71c308d](https://git.datacontroller.io/dc/dc/commit/71c308d052400ecedc03f8020a5a69471ac6b116))
|
||||||
|
|
||||||
## [7.0.3](https://git.datacontroller.io/dc/dc/compare/v7.0.2...v7.0.3) (2025-06-26)
|
## [7.0.3](https://git.datacontroller.io/dc/dc/compare/v7.0.2...v7.0.3) (2025-06-26)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -38,4 +38,27 @@ For further information:
|
|||||||
* Docs: https://docs.datacontroller.io
|
* Docs: https://docs.datacontroller.io
|
||||||
* Code: https://code.datacontroller.io
|
* Code: https://code.datacontroller.io
|
||||||
|
|
||||||
For support, contact support@4gl.io or reach out on [Matrix](https://matrix.to/#/#dc:4gl.io)!
|
For support, contact support@4gl.io or reach out on [Matrix](https://matrix.to/#/#dc:4gl.io)!
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Lighthouse CI
|
||||||
|
|
||||||
|
This project includes automated Lighthouse performance and accessibility checks that run on pull requests. The checks ensure:
|
||||||
|
|
||||||
|
- **Accessibility Score**: Minimum 1.0 (100%) median score across all tested pages
|
||||||
|
|
||||||
|
The Lighthouse CI workflow:
|
||||||
|
1. Sets up the development environment with SASjs server and mocked services
|
||||||
|
2. Builds and serves the Angular frontend
|
||||||
|
3. Runs Lighthouse CI against key application pages
|
||||||
|
4. Uploads results as artifacts for review
|
||||||
|
|
||||||
|
To run Lighthouse checks locally:
|
||||||
|
```bash
|
||||||
|
cd client
|
||||||
|
npm install
|
||||||
|
npm run lighthouse
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration is in `client/lighthouserc.js`.
|
||||||
@@ -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,22 @@
|
|||||||
"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"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"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 +101,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 +130,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 +143,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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,27 +32,26 @@ context('excel tests: ', function () {
|
|||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||||
|
|
||||||
attachExcelFile('regular.csv', () => {
|
attachExcelFile('regular.csv', () => {
|
||||||
cy.get('#approval-btn', { timeout: 60000 })
|
cy.get('#approval-btn', { timeout: 60000 }).should('be.visible')
|
||||||
.should('be.visible')
|
// .then(() => {
|
||||||
// .then(() => {
|
// cy.get('#hotInstance', { timeout: 30000 })
|
||||||
// cy.get('#hotInstance', { timeout: 30000 })
|
// .find('div.ht_master.handsontable')
|
||||||
// .find('div.ht_master.handsontable')
|
// .find('div.wtHolder')
|
||||||
// .find('div.wtHolder')
|
// .find('div.wtHider')
|
||||||
// .find('div.wtHider')
|
// .find('div.wtSpreader')
|
||||||
// .find('div.wtSpreader')
|
// .find('table.htCore')
|
||||||
// .find('table.htCore')
|
// .find('tbody')
|
||||||
// .find('tbody')
|
// .then((data) => {
|
||||||
// .then((data) => {
|
// let cell: any = data[0].children[0].children[1]
|
||||||
// let cell: any = data[0].children[0].children[1]
|
// expect(cell.innerText).to.equal('0')
|
||||||
// expect(cell.innerText).to.equal('0')
|
// cell = data[0].children[0].children[2]
|
||||||
// cell = data[0].children[0].children[2]
|
// expect(cell.innerText).to.equal('44')
|
||||||
// expect(cell.innerText).to.equal('44')
|
// cell = data[0].children[0].children[3]
|
||||||
// cell = data[0].children[0].children[3]
|
// expect(cell.innerText).to.equal('abc')
|
||||||
// expect(cell.innerText).to.equal('abc')
|
// cell = data[0].children[0].children[6]
|
||||||
// cell = data[0].children[0].children[6]
|
// expect(cell.innerText).to.equal('Option abc')
|
||||||
// expect(cell.innerText).to.equal('Option abc')
|
// })
|
||||||
// })
|
// })
|
||||||
// })
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -217,11 +217,7 @@ const rejectExcel = (callback?: any) => {
|
|||||||
.should('contain', 'Approve')
|
.should('contain', 'Approve')
|
||||||
.then((allButtons: any) => {
|
.then((allButtons: any) => {
|
||||||
for (let approvalButton of allButtons) {
|
for (let approvalButton of allButtons) {
|
||||||
if (
|
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||||
approvalButton.innerText
|
|
||||||
.toLowerCase()
|
|
||||||
.includes('approve')
|
|
||||||
) {
|
|
||||||
approvalButton.click()
|
approvalButton.click()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,93 +34,162 @@ context('excel multi load tests: ', function () {
|
|||||||
|
|
||||||
it('1 | Uploads Excel file with multiple sheets, 3 sheets including data, 2 sheets matched with dataset', (done) => {
|
it('1 | Uploads Excel file with multiple sheets, 3 sheets including data, 2 sheets matched with dataset', (done) => {
|
||||||
attachExcelFile('multi_load_test_2.xlsx', () => {
|
attachExcelFile('multi_load_test_2.xlsx', () => {
|
||||||
checkHotUserDatasetTable('hotTableUserDataset', [
|
checkHotUserDatasetTable(
|
||||||
[library, mpeXTestTable],
|
'hotTableUserDataset',
|
||||||
[library, mpeTablesTable]
|
[
|
||||||
], () => {
|
[library, mpeXTestTable],
|
||||||
cy.get('#continue-btn').trigger('click').then(() => {
|
[library, mpeTablesTable]
|
||||||
checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], undefined, (includes: boolean) => {
|
],
|
||||||
if (includes) {
|
() => {
|
||||||
// MPE_TABLES sheet does not have data so 1 error image must be shown
|
cy.get('#continue-btn')
|
||||||
hasErrorTables(1, (valid: boolean) => {
|
.trigger('click')
|
||||||
if (valid) done()
|
.then(() => {
|
||||||
})
|
checkIfTreeHasTables(
|
||||||
}
|
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
|
||||||
})
|
undefined,
|
||||||
})
|
(includes: boolean) => {
|
||||||
})
|
if (includes) {
|
||||||
|
// MPE_TABLES sheet does not have data so 1 error image must be shown
|
||||||
|
hasErrorTables(1, (valid: boolean) => {
|
||||||
|
if (valid) done()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('2 | Uploads Excel file with multiple sheets, 2 sheets matched with dataset, 1 matched sheet does not have data', (done) => {
|
it('2 | Uploads Excel file with multiple sheets, 2 sheets matched with dataset, 1 matched sheet does not have data', (done) => {
|
||||||
attachExcelFile('multi_load_test_1.xlsx', () => {
|
attachExcelFile('multi_load_test_1.xlsx', () => {
|
||||||
checkHotUserDatasetTable('hotTableUserDataset', [
|
checkHotUserDatasetTable(
|
||||||
[library, mpeXTestTable],
|
'hotTableUserDataset',
|
||||||
[library, mpeTablesTable]
|
[
|
||||||
], () => {
|
[library, mpeXTestTable],
|
||||||
cy.get('#continue-btn').trigger('click').then(() => {
|
[library, mpeTablesTable]
|
||||||
checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], `${library}.${mpeXTestTable}`, (includes: boolean) => {
|
],
|
||||||
if (includes) {
|
() => {
|
||||||
cy.get('#hotTable').should('be.visible').then(() => {
|
cy.get('#continue-btn')
|
||||||
checkHotUserDatasetTable('hotTable', [
|
.trigger('click')
|
||||||
['No', '1', 'more dummy data'],
|
.then(() => {
|
||||||
['No', '1', '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:'],
|
checkIfTreeHasTables(
|
||||||
['No', '1', 'if you can fill the unforgiving minute']
|
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
|
||||||
], () => {
|
`${library}.${mpeXTestTable}`,
|
||||||
submitTables()
|
(includes: boolean) => {
|
||||||
|
if (includes) {
|
||||||
|
cy.get('#hotTable')
|
||||||
|
.should('be.visible')
|
||||||
|
.then(() => {
|
||||||
|
checkHotUserDatasetTable(
|
||||||
|
'hotTable',
|
||||||
|
[
|
||||||
|
['No', '1', 'more dummy data'],
|
||||||
|
[
|
||||||
|
'No',
|
||||||
|
'1',
|
||||||
|
'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:'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'No',
|
||||||
|
'1',
|
||||||
|
'if you can fill the unforgiving minute'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
submitTables()
|
||||||
|
|
||||||
hasSuccessSubmits(2, (valid: boolean) => {
|
hasSuccessSubmits(2, (valid: boolean) => {
|
||||||
if (valid) done()
|
if (valid) done()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('3 | Uploads Excel file with multiple sheets, 1 sheets has 2 tables', (done) => {
|
it('3 | Uploads Excel file with multiple sheets, 1 sheets has 2 tables', (done) => {
|
||||||
attachExcelFile('multi_load_test_1.xlsx', () => {
|
attachExcelFile('multi_load_test_1.xlsx', () => {
|
||||||
checkHotUserDatasetTable('hotTableUserDataset', [
|
checkHotUserDatasetTable(
|
||||||
[library, mpeXTestTable],
|
'hotTableUserDataset',
|
||||||
[library, mpeTablesTable]
|
[
|
||||||
], () => {
|
[library, mpeXTestTable],
|
||||||
cy.get('#continue-btn').trigger('click').then(() => {
|
[library, mpeTablesTable]
|
||||||
checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], `${library}.${mpeXTestTable}`, (includes: boolean) => {
|
],
|
||||||
if (includes) {
|
() => {
|
||||||
cy.get('#hotTable').should('be.visible').then(() => {
|
cy.get('#continue-btn')
|
||||||
checkHotUserDatasetTable('hotTable', [
|
.trigger('click')
|
||||||
['No', '1', 'more dummy data'],
|
.then(() => {
|
||||||
['No', '1', '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:'],
|
checkIfTreeHasTables(
|
||||||
['No', '1', 'if you can fill the unforgiving minute']
|
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
|
||||||
], () => {
|
`${library}.${mpeXTestTable}`,
|
||||||
clickOnTreeNode('DC996664.MPE_TABLES', () => {
|
(includes: boolean) => {
|
||||||
cy.wait(1000).then(() => {
|
if (includes) {
|
||||||
cy.get('#hotTable').should('be.visible').then(() => {
|
cy.get('#hotTable')
|
||||||
checkHotUserDatasetTable('hotTable', [
|
.should('be.visible')
|
||||||
['No', 'DC914286', 'MPE_COLUMN_LEVEL_SECURITY'],
|
.then(() => {
|
||||||
['No', 'DC914286', 'MPE_XLMAP_INFO'],
|
checkHotUserDatasetTable(
|
||||||
['No', 'DC914286', 'MPE_XLMAP_RULES']
|
'hotTable',
|
||||||
], () => {
|
[
|
||||||
submitTables()
|
['No', '1', 'more dummy data'],
|
||||||
|
[
|
||||||
|
'No',
|
||||||
|
'1',
|
||||||
|
'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:'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'No',
|
||||||
|
'1',
|
||||||
|
'if you can fill the unforgiving minute'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
clickOnTreeNode('DC996664.MPE_TABLES', () => {
|
||||||
|
cy.wait(1000).then(() => {
|
||||||
|
cy.get('#hotTable')
|
||||||
|
.should('be.visible')
|
||||||
|
.then(() => {
|
||||||
|
checkHotUserDatasetTable(
|
||||||
|
'hotTable',
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'No',
|
||||||
|
'DC914286',
|
||||||
|
'MPE_COLUMN_LEVEL_SECURITY'
|
||||||
|
],
|
||||||
|
['No', 'DC914286', 'MPE_XLMAP_INFO'],
|
||||||
|
['No', 'DC914286', 'MPE_XLMAP_RULES']
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
submitTables()
|
||||||
|
|
||||||
hasSuccessSubmits(2, (valid: boolean) => {
|
hasSuccessSubmits(
|
||||||
if (valid) done()
|
2,
|
||||||
})
|
(valid: boolean) => {
|
||||||
|
if (valid) done()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
})
|
}
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -142,25 +211,31 @@ const attachExcelFile = (excelFilename: string, callback?: any) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkHotUserDatasetTable = (hotId: string, dataToContain: any[][], callback?: () => void) => {
|
const checkHotUserDatasetTable = (
|
||||||
|
hotId: string,
|
||||||
|
dataToContain: any[][],
|
||||||
|
callback?: () => void
|
||||||
|
) => {
|
||||||
cy.get(`#${hotId}`, { timeout: longerCommandTimeout })
|
cy.get(`#${hotId}`, { timeout: longerCommandTimeout })
|
||||||
.find('div.ht_master.handsontable')
|
.find('div.ht_master.handsontable')
|
||||||
.find('div.wtHolder')
|
.find('div.wtHolder')
|
||||||
.find('div.wtHider')
|
.find('div.wtHider')
|
||||||
.find('div.wtSpreader')
|
.find('div.wtSpreader')
|
||||||
.find('table.htCore')
|
.find('table.htCore')
|
||||||
.find('tbody')
|
.find('tbody')
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
cy.wait(2000).then(() => {
|
cy.wait(2000).then(() => {
|
||||||
for (let rowI = 0; rowI < dataToContain.length; rowI++) {
|
for (let rowI = 0; rowI < dataToContain.length; rowI++) {
|
||||||
for (let colI = 0; colI < dataToContain[rowI].length; colI++) {
|
for (let colI = 0; colI < dataToContain[rowI].length; colI++) {
|
||||||
expect(data[0].children[rowI].children[colI]).to.contain(dataToContain[rowI][colI])
|
expect(data[0].children[rowI].children[colI]).to.contain(
|
||||||
|
dataToContain[rowI][colI]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (callback) callback()
|
if (callback) callback()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const clickOnTreeNode = (clickOnNode: string, callback?: () => void) => {
|
const clickOnTreeNode = (clickOnNode: string, callback?: () => void) => {
|
||||||
@@ -174,7 +249,11 @@ const clickOnTreeNode = (clickOnNode: string, callback?: () => void) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkIfTreeHasTables = (tables: string[], clickOnNode?: string, callback?: (includes: boolean) => void) => {
|
const checkIfTreeHasTables = (
|
||||||
|
tables: string[],
|
||||||
|
clickOnNode?: string,
|
||||||
|
callback?: (includes: boolean) => void
|
||||||
|
) => {
|
||||||
cy.get('.nav-tree clr-tree > clr-tree-node').then((treeNodes: any) => {
|
cy.get('.nav-tree clr-tree > clr-tree-node').then((treeNodes: any) => {
|
||||||
let datasets = tables
|
let datasets = tables
|
||||||
let nodesCorrect = true
|
let nodesCorrect = true
|
||||||
@@ -207,16 +286,26 @@ const submitTables = () => {
|
|||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasSuccessSubmits = (expectedNoOfSubmits: number, callback: (valid: boolean) => void) => {
|
const hasSuccessSubmits = (
|
||||||
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="success"]').should('be.visible').then(($nodes) => {
|
expectedNoOfSubmits: number,
|
||||||
callback(expectedNoOfSubmits === $nodes.length)
|
callback: (valid: boolean) => void
|
||||||
})
|
) => {
|
||||||
|
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="success"]')
|
||||||
|
.should('be.visible')
|
||||||
|
.then(($nodes) => {
|
||||||
|
callback(expectedNoOfSubmits === $nodes.length)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasErrorTables = (expectedNoOfErrors: number, callback: (valid: boolean) => void) => {
|
const hasErrorTables = (
|
||||||
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="danger"]').should('be.visible').then(($nodes) => {
|
expectedNoOfErrors: number,
|
||||||
callback(expectedNoOfErrors === $nodes.length)
|
callback: (valid: boolean) => void
|
||||||
})
|
) => {
|
||||||
|
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="danger"]')
|
||||||
|
.should('be.visible')
|
||||||
|
.then(($nodes) => {
|
||||||
|
callback(expectedNoOfErrors === $nodes.length)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const visitPage = (url: string) => {
|
const visitPage = (url: string) => {
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ context('excel tests: ', function () {
|
|||||||
cy.get('.btn-upload-preview', { timeout: 60000 })
|
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
cy.get('#hotInstance', { timeout: 30000 })
|
cy.get('#hotTable', { timeout: 30000 })
|
||||||
.find('div.ht_master.handsontable')
|
.find('div.ht_master.handsontable')
|
||||||
.find('div.wtHolder')
|
.find('div.wtHolder')
|
||||||
.find('div.wtHider')
|
.find('div.wtHider')
|
||||||
@@ -283,7 +283,7 @@ context('excel tests: ', function () {
|
|||||||
cy.get('.btn-upload-preview', { timeout: 60000 })
|
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
cy.get('#hotInstance', { timeout: 30000 })
|
cy.get('#hotTable', { timeout: 30000 })
|
||||||
.find('div.ht_master.handsontable')
|
.find('div.ht_master.handsontable')
|
||||||
.find('div.wtHolder')
|
.find('div.wtHolder')
|
||||||
.find('div.wtHider')
|
.find('div.wtHider')
|
||||||
@@ -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) => {
|
||||||
@@ -399,11 +476,7 @@ const rejectExcel = (callback?: any) => {
|
|||||||
.should('contain', 'Approve')
|
.should('contain', 'Approve')
|
||||||
.then((allButtons: any) => {
|
.then((allButtons: any) => {
|
||||||
for (let approvalButton of allButtons) {
|
for (let approvalButton of allButtons) {
|
||||||
if (
|
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||||
approvalButton.innerText
|
|
||||||
.toLowerCase()
|
|
||||||
.includes('approve')
|
|
||||||
) {
|
|
||||||
approvalButton.click()
|
approvalButton.click()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -432,11 +505,7 @@ const acceptExcel = (callback?: any) => {
|
|||||||
.should('contain', 'Approve')
|
.should('contain', 'Approve')
|
||||||
.then((allButtons: any) => {
|
.then((allButtons: any) => {
|
||||||
for (let approvalButton of allButtons) {
|
for (let approvalButton of allButtons) {
|
||||||
if (
|
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||||
approvalButton.innerText
|
|
||||||
.toLowerCase()
|
|
||||||
.includes('approve')
|
|
||||||
) {
|
|
||||||
approvalButton.click()
|
approvalButton.click()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -455,7 +524,7 @@ const acceptExcel = (callback?: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checkResultOfFormulaUpload = (callback?: any) => {
|
const checkResultOfFormulaUpload = (callback?: any) => {
|
||||||
cy.get('#hotInstance', { timeout: longerCommandTimeout })
|
cy.get('#hotTable', { timeout: longerCommandTimeout })
|
||||||
.find('div.ht_master.handsontable')
|
.find('div.ht_master.handsontable')
|
||||||
.find('div.wtHolder')
|
.find('div.wtHolder')
|
||||||
.find('div.wtHider')
|
.find('div.wtHider')
|
||||||
@@ -471,7 +540,7 @@ const checkResultOfFormulaUpload = (callback?: any) => {
|
|||||||
|
|
||||||
const checkResultOfXLSUpload = (callback?: any) => {
|
const checkResultOfXLSUpload = (callback?: any) => {
|
||||||
cy.viewport(1280, 720)
|
cy.viewport(1280, 720)
|
||||||
cy.get('#hotInstance', { timeout: 30000 })
|
cy.get('#hotTable', { timeout: 30000 })
|
||||||
.find('div.ht_master.handsontable')
|
.find('div.ht_master.handsontable')
|
||||||
.find('div.wtHolder')
|
.find('div.wtHolder')
|
||||||
.find('div.wtHider')
|
.find('div.wtHider')
|
||||||
@@ -500,7 +569,7 @@ const checkResultOfXLSUpload = (callback?: any) => {
|
|||||||
if (callback) callback()
|
if (callback) callback()
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('#hotInstance', { timeout: 30000 })
|
cy.get('#hotTable', { timeout: 30000 })
|
||||||
.find('div.ht_master.handsontable')
|
.find('div.ht_master.handsontable')
|
||||||
.find('div.wtHolder')
|
.find('div.wtHolder')
|
||||||
.scrollTo('right')
|
.scrollTo('right')
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ context('filtering tests: ', function () {
|
|||||||
this.beforeEach(() => {
|
this.beforeEach(() => {
|
||||||
cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout })
|
cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout })
|
||||||
|
|
||||||
|
|
||||||
visitPage('home')
|
visitPage('home')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -299,14 +298,16 @@ const setFilterWithValue = (
|
|||||||
cy.get('.no-values')
|
cy.get('.no-values')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
cy.get('.in-values-modal clr-checkbox-wrapper input').then((inputs: any) => {
|
cy.get('.in-values-modal clr-checkbox-wrapper input').then(
|
||||||
inputs[0].click()
|
(inputs: any) => {
|
||||||
cy.get('.in-values-modal .modal-footer button').click()
|
inputs[0].click()
|
||||||
|
cy.get('.in-values-modal .modal-footer button').click()
|
||||||
|
|
||||||
cy.get('.modal-footer .btn-success-outline').click()
|
cy.get('.modal-footer .btn-success-outline').click()
|
||||||
|
|
||||||
if (callback) callback()
|
if (callback) callback()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ interface EditConfigTableCells {
|
|||||||
|
|
||||||
context('licensing tests: ', function () {
|
context('licensing tests: ', function () {
|
||||||
this.beforeAll(() => {
|
this.beforeAll(() => {
|
||||||
|
|
||||||
cy.loginAndUpdateValidKey()
|
cy.loginAndUpdateValidKey()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -371,8 +370,6 @@ context('licensing tests: ', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const logout = (callback?: any) => {
|
const logout = (callback?: any) => {
|
||||||
@@ -697,11 +694,7 @@ const approveTable = (callback?: any) => {
|
|||||||
.should('contain', 'Approve')
|
.should('contain', 'Approve')
|
||||||
.then((allButtons: any) => {
|
.then((allButtons: any) => {
|
||||||
for (let approvalButton of allButtons) {
|
for (let approvalButton of allButtons) {
|
||||||
if (
|
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||||
approvalButton.innerText
|
|
||||||
.toLowerCase()
|
|
||||||
.includes('approve')
|
|
||||||
) {
|
|
||||||
approvalButton.click()
|
approvalButton.click()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ context('liveness tests: ', function () {
|
|||||||
this.beforeEach(() => {
|
this.beforeEach(() => {
|
||||||
cy.visit(hostUrl + appLocation)
|
cy.visit(hostUrl + appLocation)
|
||||||
|
|
||||||
|
|
||||||
visitPage('home')
|
visitPage('home')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -125,11 +124,7 @@ const rejectExcel = (callback?: any) => {
|
|||||||
.should('contain', 'Approve')
|
.should('contain', 'Approve')
|
||||||
.then((allButtons: any) => {
|
.then((allButtons: any) => {
|
||||||
for (let approvalButton of allButtons) {
|
for (let approvalButton of allButtons) {
|
||||||
if (
|
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||||
approvalButton.innerText
|
|
||||||
.toLowerCase()
|
|
||||||
.includes('approve')
|
|
||||||
) {
|
|
||||||
approvalButton.click()
|
approvalButton.click()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ context('editor tests: ', function () {
|
|||||||
cy.get('.viewbox-open').click()
|
cy.get('.viewbox-open').click()
|
||||||
openTableFromViewboxTree(
|
openTableFromViewboxTree(
|
||||||
libraryToOpenIncludes,
|
libraryToOpenIncludes,
|
||||||
viewboxes.map((viewbox) => viewbox.viewbox_table))
|
viewboxes.map((viewbox) => viewbox.viewbox_table)
|
||||||
|
)
|
||||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||||
let found = 0
|
let found = 0
|
||||||
|
|
||||||
@@ -91,32 +92,34 @@ context('editor tests: ', function () {
|
|||||||
|
|
||||||
if (found < viewboxes.length) return
|
if (found < viewboxes.length) return
|
||||||
|
|
||||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then((viewboxNodes: any) => {
|
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
|
||||||
for (let viewboxNode of viewboxNodes) {
|
(viewboxNodes: any) => {
|
||||||
cy.get(viewboxNode).within(() => {
|
for (let viewboxNode of viewboxNodes) {
|
||||||
cy.get('.table-title').then((tableTitle) => {
|
cy.get(viewboxNode).within(() => {
|
||||||
const title = tableTitle[0].innerText
|
cy.get('.table-title').then((tableTitle) => {
|
||||||
const viewbox = viewboxes.find((vb) =>
|
const title = tableTitle[0].innerText
|
||||||
title.toLowerCase().includes(vb.viewbox_table)
|
const viewbox = viewboxes.find((vb) =>
|
||||||
)
|
title.toLowerCase().includes(vb.viewbox_table)
|
||||||
|
|
||||||
if (viewbox) {
|
|
||||||
cy.get('.ht_master.handsontable .htCore thead tr').then(
|
|
||||||
(viewboxColNodes: any) => {
|
|
||||||
let allColsHtml = viewboxColNodes[0].innerHTML
|
|
||||||
|
|
||||||
for (let col of viewbox?.columns) {
|
|
||||||
if (!allColsHtml.includes(col)) return
|
|
||||||
}
|
|
||||||
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
if (viewbox) {
|
||||||
|
cy.get('.ht_master.handsontable .htCore thead tr').then(
|
||||||
|
(viewboxColNodes: any) => {
|
||||||
|
let allColsHtml = viewboxColNodes[0].innerHTML
|
||||||
|
|
||||||
|
for (let col of viewbox?.columns) {
|
||||||
|
if (!allColsHtml.includes(col)) return
|
||||||
|
}
|
||||||
|
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -395,11 +398,13 @@ context('editor tests: ', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const removeAllColumns = () => {
|
const removeAllColumns = () => {
|
||||||
cy.get('.configuration-wrapper clr-icon[shape="trash"]').then(removeNodes => {
|
cy.get('.configuration-wrapper clr-icon[shape="trash"]').then(
|
||||||
for (let removeNode of removeNodes) {
|
(removeNodes) => {
|
||||||
removeNode.click()
|
for (let removeNode of removeNodes) {
|
||||||
|
removeNode.click()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkColumns = (columns: string[], callback: () => void) => {
|
const checkColumns = (columns: string[], callback: () => void) => {
|
||||||
@@ -412,7 +417,7 @@ const checkColumns = (columns: string[], callback: () => void) => {
|
|||||||
console.log('viewboxColNode', viewboxColNodes)
|
console.log('viewboxColNode', viewboxColNodes)
|
||||||
console.log('columns', columns)
|
console.log('columns', columns)
|
||||||
for (let i = 0; i < viewboxColNodes.length; i++) {
|
for (let i = 0; i < viewboxColNodes.length; i++) {
|
||||||
const col = columns[i]|| ''
|
const col = columns[i] || ''
|
||||||
const colNode = viewboxColNodes[i]
|
const colNode = viewboxColNodes[i]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
BIN
client/cypress/fixtures/excels/regular_excel_password.xlsx
Normal file
BIN
client/cypress/fixtures/excels/regular_excel_password.xlsx
Normal file
Binary file not shown.
@@ -1,255 +0,0 @@
|
|||||||
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 = 'excels_general/'
|
|
||||||
const downloadsFolder = Cypress.config('downloadsFolder')
|
|
||||||
|
|
||||||
import { deleteDownloadsFolder } from '../util/deleteDownloadFolder'
|
|
||||||
|
|
||||||
context('download files test: ', function () {
|
|
||||||
this.beforeAll(() => {
|
|
||||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
|
||||||
cy.loginAndUpdateValidKey()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.beforeEach(() => {
|
|
||||||
cy.visit(hostUrl + appLocation)
|
|
||||||
cy.get('input.username').type(username)
|
|
||||||
cy.get('input.password').type(password)
|
|
||||||
cy.get('.login-group button').click()
|
|
||||||
|
|
||||||
visitPage('home')
|
|
||||||
})
|
|
||||||
|
|
||||||
this.afterEach(() => {
|
|
||||||
deleteDownloadsFolder()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('1 | downloads audit file', (done) => {
|
|
||||||
visitPage('approve/toapprove')
|
|
||||||
|
|
||||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
|
||||||
.should('not.exist')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.btn.btn-success')
|
|
||||||
.should('be.visible')
|
|
||||||
.then((buttons) => {
|
|
||||||
buttons[0].click()
|
|
||||||
const id = buttons[0].id
|
|
||||||
|
|
||||||
checkForFileDownloaded(id, 'zip', () => done())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('2 | downloads viewer csv', (done) => {
|
|
||||||
visitPage('view/data')
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openDownloadModal(() => {
|
|
||||||
cy.get('select')
|
|
||||||
.select('CSV')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
|
||||||
button.trigger('click')
|
|
||||||
|
|
||||||
const id = button[0].id
|
|
||||||
|
|
||||||
checkForFileDownloaded(id, 'csv', () => done())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('3 | downloads viewer excel', (done) => {
|
|
||||||
visitPage('view/data')
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openDownloadModal(() => {
|
|
||||||
cy.get('select')
|
|
||||||
.select('Excel')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
|
||||||
button.trigger('click')
|
|
||||||
|
|
||||||
const id = button[0].id
|
|
||||||
|
|
||||||
checkForFileDownloaded(id, 'xlsx', () => done())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('4 | downloads viewer SAS Datalines', (done) => {
|
|
||||||
visitPage('view/data')
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openDownloadModal(() => {
|
|
||||||
cy.get('select')
|
|
||||||
.select('SAS Datalines')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
|
||||||
button.trigger('click')
|
|
||||||
|
|
||||||
const id = button[0].id
|
|
||||||
|
|
||||||
checkForFileDownloaded(id, 'sas', () => done())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('5 | downloads viewer SAS DDL', (done) => {
|
|
||||||
visitPage('view/data')
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openDownloadModal(() => {
|
|
||||||
cy.get('select')
|
|
||||||
.select('SAS DDL')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
|
||||||
button.trigger('click')
|
|
||||||
|
|
||||||
const id = button[0].id
|
|
||||||
|
|
||||||
checkForFileDownloaded(id, 'ddl', () => done(), '_')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('6 | downloads viewer TSQL DDL', (done) => {
|
|
||||||
visitPage('view/data')
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openDownloadModal(() => {
|
|
||||||
cy.get('select')
|
|
||||||
.select('TSQL DDL')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
|
||||||
button.trigger('click')
|
|
||||||
|
|
||||||
const id = button[0].id
|
|
||||||
|
|
||||||
checkForFileDownloaded(id, 'ddl', () => done(), '_')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('7 | downloads viewer PGSQL DDL', (done) => {
|
|
||||||
visitPage('view/data')
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openDownloadModal(() => {
|
|
||||||
cy.get('select')
|
|
||||||
.select('PGSQL DDL')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
|
||||||
button.trigger('click')
|
|
||||||
|
|
||||||
const id = button[0].id
|
|
||||||
|
|
||||||
checkForFileDownloaded(id, 'ddl', () => done(), '_')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.afterEach(() => {
|
|
||||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.afterAll(() => {
|
|
||||||
cy.visit(`https://sas.4gl.io/mihmed/cypress_finish`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const visitPage = (url: string) => {
|
|
||||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkForFileDownloaded = (
|
|
||||||
id: string,
|
|
||||||
extension: string,
|
|
||||||
callback?: any,
|
|
||||||
libDivider: string = '.'
|
|
||||||
) => {
|
|
||||||
cy.on('url:changed', (newUrl) => {
|
|
||||||
console.log('newUrl', newUrl)
|
|
||||||
})
|
|
||||||
|
|
||||||
id = id.replace('.', libDivider)
|
|
||||||
|
|
||||||
const filename = downloadsFolder + '/' + id + '.' + extension
|
|
||||||
// browser might take a while to download the file,
|
|
||||||
// so use "cy.readFile" to retry until the file exists
|
|
||||||
// and has length - and we assume that it has finished downloading then
|
|
||||||
cy.readFile(filename, { timeout: longerCommandTimeout })
|
|
||||||
.should('have.length.gt', 10)
|
|
||||||
.then((file) => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const openDownloadModal = (callback?: any) => {
|
|
||||||
cy.get('.btn.btn-sm.btn-outline.filterSide.dropdown-toggle')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('clr-dropdown-menu button').then((buttons) => {
|
|
||||||
for (let button of buttons) {
|
|
||||||
if (button.innerText.toLowerCase().includes('download')) {
|
|
||||||
button.click()
|
|
||||||
|
|
||||||
if (callback) callback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 viyaLib
|
|
||||||
|
|
||||||
for (let node of treeNodes) {
|
|
||||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
|
||||||
viyaLib = node
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('viyaLib', viyaLib)
|
|
||||||
|
|
||||||
cy.get(viyaLib).within(() => {
|
|
||||||
cy.get(
|
|
||||||
'.clr-tree-node-content-container .clr-treenode-content p'
|
|
||||||
).click()
|
|
||||||
|
|
||||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
|
||||||
for (let innerNode of innerNodes) {
|
|
||||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
|
||||||
innerNode.click()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
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 = 'excels_general/'
|
|
||||||
|
|
||||||
context('editor tests: ', function () {
|
|
||||||
this.beforeAll(() => {
|
|
||||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
|
||||||
cy.loginAndUpdateValidKey()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.beforeEach(() => {
|
|
||||||
cy.visit(hostUrl + appLocation)
|
|
||||||
|
|
||||||
visitPage('home')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('1 | Submits duplicate primary keys', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
|
|
||||||
|
|
||||||
attachExcelFile('MPE_DATADICTIONARY_duplicate_keys.xlsx', () => {
|
|
||||||
clickOnUploadPreview(() => {
|
|
||||||
confirmEditPreviewFile(() => {
|
|
||||||
submitTable(() => {
|
|
||||||
cy.get('.modal-body').then((modalBody: any) => {
|
|
||||||
if (modalBody[0].innerText.includes(`Duplicates found:`)) {
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('2 | Submits null cells which must not be null', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
clickOnEdit(() => {
|
|
||||||
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
||||||
timeout: longerCommandTimeout
|
|
||||||
}).then(() => {
|
|
||||||
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
||||||
cy.get(rows[1].childNodes[2])
|
|
||||||
.dblclick({ force: true })
|
|
||||||
.then(() => {
|
|
||||||
cy.focused()
|
|
||||||
.clear()
|
|
||||||
.type('{enter}')
|
|
||||||
.then(() => {
|
|
||||||
submitTable(() => {
|
|
||||||
cy.get('.modal-body').then((modalBody: any) => {
|
|
||||||
if (
|
|
||||||
modalBody[0].innerHTML
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(`invalid values are present`)
|
|
||||||
) {
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('3 | Gets basic dynamic cell validation', () => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
clickOnEdit(() => {
|
|
||||||
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
||||||
timeout: longerCommandTimeout
|
|
||||||
}).then(() => {
|
|
||||||
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
||||||
cy.get(rows[1].childNodes[5])
|
|
||||||
.click({ force: true })
|
|
||||||
.then(($td) => {
|
|
||||||
cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
|
|
||||||
'exist'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('4 | Gets advanced dynamic cell validation', () => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_tables')
|
|
||||||
|
|
||||||
clickOnEdit(() => {
|
|
||||||
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
|
||||||
timeout: longerCommandTimeout
|
|
||||||
}).then(() => {
|
|
||||||
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
||||||
cy.get(rows[1].childNodes[3])
|
|
||||||
.click({ force: true })
|
|
||||||
.then(($td) => {
|
|
||||||
cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
|
|
||||||
'exist'
|
|
||||||
)
|
|
||||||
cy.get('.htAutocompleteArrow', {
|
|
||||||
withinSubject: rows[1].childNodes[7]
|
|
||||||
}).should('exist')
|
|
||||||
cy.get('.htAutocompleteArrow', {
|
|
||||||
withinSubject: rows[1].childNodes[8]
|
|
||||||
}).should('exist')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const clickOnEdit = (callback?: any) => {
|
|
||||||
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 viyaLib
|
|
||||||
|
|
||||||
for (let node of treeNodes) {
|
|
||||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
|
||||||
viyaLib = node
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.get(viyaLib).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 attachExcelFile = (excelFilename: string, callback?: any) => {
|
|
||||||
cy.get('.buttonBar button:last-child')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('input[type="file"]#file-upload')
|
|
||||||
.attachFile(`/${fixturePath}/${excelFilename}`)
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => {
|
|
||||||
modalBtn.click()
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const clickOnUploadPreview = (callback?: any) => {
|
|
||||||
cy.get('.buttonBar button.btn-primary.btn-upload-preview')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmEditPreviewFile = (callback?: any) => {
|
|
||||||
cy.get('.modal-footer button.btn-success-outline')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitTable = (callback?: any) => {
|
|
||||||
cy.get('.btnCtrl button.btn-primary')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitTableMessage = (callback?: any) => {
|
|
||||||
cy.get('.modal-footer .btn.btn-sm.btn-success-outline')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitExcel = (callback?: any) => {
|
|
||||||
cy.get('.buttonBar button.preview-submit')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const rejectExcel = (callback?: any) => {
|
|
||||||
cy.get('button', { timeout: longerCommandTimeout })
|
|
||||||
.should('contain', 'Approve')
|
|
||||||
.then((allButtons: any) => {
|
|
||||||
for (let approvalButton of allButtons) {
|
|
||||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
|
||||||
approvalButton.click()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.get('button.btn-danger')
|
|
||||||
.should('exist')
|
|
||||||
.should('not.be.disabled')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.modal-footer button.btn-success-outline')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('app-history')
|
|
||||||
.should('exist')
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const visitPage = (url: string) => {
|
|
||||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
||||||
}
|
|
||||||
@@ -1,527 +0,0 @@
|
|||||||
import { Callbacks } from 'cypress/types/jquery/index'
|
|
||||||
|
|
||||||
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 = 'excels/'
|
|
||||||
|
|
||||||
// TODO: 4 and 9 failing
|
|
||||||
|
|
||||||
context('excel tests: ', function () {
|
|
||||||
this.beforeAll(() => {
|
|
||||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
|
||||||
cy.loginAndUpdateValidKey()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.beforeEach(() => {
|
|
||||||
cy.visit(hostUrl + appLocation)
|
|
||||||
|
|
||||||
visitPage('home')
|
|
||||||
|
|
||||||
colorLog(
|
|
||||||
`TEST START ---> ${
|
|
||||||
Cypress.mocha.getRunner().suite.ctx.currentTest.title
|
|
||||||
}`,
|
|
||||||
'#3498DB'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('1 | Uploads regular Excel file', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('regular_excel.xlsx', () => {
|
|
||||||
submitExcel()
|
|
||||||
rejectExcel(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('2 | Uploads Excel with data on the 7th tab', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('7th_tab_excel.xlsx', () => {
|
|
||||||
submitExcel()
|
|
||||||
rejectExcel(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('3 | Uploads Excel with missing columns (should fail)', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('missing_columns_excel.xlsx', () => {
|
|
||||||
cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
|
||||||
.should('exist')
|
|
||||||
.then((elements: any) => {
|
|
||||||
if (elements[0]) {
|
|
||||||
if (elements[0].innerText.toLowerCase().includes('missing')) done()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('4 | Uploads Excel with formulas', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
|
|
||||||
|
|
||||||
attachExcelFile('formulas_excel.xlsx', () => {
|
|
||||||
checkResultOfFormulaUpload(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('5 | Uploads Excel with no data rows', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('nodata_rows_excel.xlsx', () => {
|
|
||||||
cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
|
||||||
.should('exist')
|
|
||||||
.then((elements: any) => {
|
|
||||||
if (elements[0]) {
|
|
||||||
if (
|
|
||||||
elements[0].innerText
|
|
||||||
.toLowerCase()
|
|
||||||
.includes('no relevant data found')
|
|
||||||
)
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('6 | Uploads Excel with a table that is surrounded by other data', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('surrounded_data_excel.xlsx', () => {
|
|
||||||
submitExcel()
|
|
||||||
rejectExcel(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('7 | Uploads Excel with a extra columns in the middle', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('extra_column_excel.xlsx', () => {
|
|
||||||
submitExcel()
|
|
||||||
rejectExcel(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('8 | Uploads Excel with a duplicate column', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('duplicate_column_excel.xlsx', () => {
|
|
||||||
cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
|
||||||
.should('exist')
|
|
||||||
.then((elements: any) => {
|
|
||||||
if (elements[0]) {
|
|
||||||
if (elements[0].innerText.toLowerCase().includes('missing')) done()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// it('9 | Uploads Excel with a duplicate row', (done) => {
|
|
||||||
// openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
// attachExcelFile('duplicate_row_excel.xlsx', () => {
|
|
||||||
// submitExcel(() => {
|
|
||||||
// cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
|
||||||
// .should('exist')
|
|
||||||
// .then((elements: any) => {
|
|
||||||
// if (elements[0]) {
|
|
||||||
// if (elements[0].innerText.toLowerCase().includes('duplicates'))
|
|
||||||
// done()
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
it('10 | Uploads Excel with a mixed content', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('mixed_content_excel.xlsx', () => {
|
|
||||||
submitExcel(() => {
|
|
||||||
cy.get('.modal-body').then((modalBody: any) => {
|
|
||||||
if (
|
|
||||||
modalBody[0].innerHTML
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(`invalid values are present`)
|
|
||||||
) {
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('11 | Uploads Excel with a blank columns', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('blank_columns_excel.xlsx', () => {
|
|
||||||
cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
|
||||||
.should('exist')
|
|
||||||
.then((elements: any) => {
|
|
||||||
if (elements[0]) {
|
|
||||||
if (elements[0].innerText.toLowerCase().includes('missing')) done()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('12 | Uploads Excel xls extension', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('regular_excel_xls.xls', () => {
|
|
||||||
submitExcel()
|
|
||||||
rejectExcel(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// For some strange reason this file breaks cypress. When uploaded manually in DC it is working.
|
|
||||||
// it('13 | Uploads Excel xlsm extension', (done) => {
|
|
||||||
// openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
// attachExcelFile('regular_excel_macro.xlsm', () => {
|
|
||||||
// submitExcel()
|
|
||||||
// rejectExcel(done)
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
it('14 | Uploads Excel with composite primary key', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
|
|
||||||
|
|
||||||
attachExcelFile('MPE_DATADICTIONARY_composite_keys.xlsx', () => {
|
|
||||||
submitExcel()
|
|
||||||
rejectExcel(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('15 | Uploads Excel with missing row (empty table)', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
|
|
||||||
|
|
||||||
attachExcelFile('MPE_DATADICTIONARY_missing_row.xlsx', () => {
|
|
||||||
cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
|
||||||
.should('exist')
|
|
||||||
.then((elements: any) => {
|
|
||||||
if (elements[0]) {
|
|
||||||
if (
|
|
||||||
elements[0].innerText
|
|
||||||
.toLowerCase()
|
|
||||||
.includes('no relevant data found')
|
|
||||||
)
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('16 | Uploads Excel with merged cells', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
|
|
||||||
|
|
||||||
attachExcelFile('MPE_DATADICTIONARY_merged_cells.xlsx', () => {
|
|
||||||
submitExcel()
|
|
||||||
rejectExcel(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('17 | Check uploaded values from excel with xls extension', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('regular_excel_xls.xls', () => {
|
|
||||||
checkResultOfXLSUpload(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('18 | Uploads Excel with missing row (empty table)', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('blank_column_with_header.xlsx', () => {
|
|
||||||
cy.get('.btn-upload-preview', { timeout: 60000 })
|
|
||||||
.should('be.visible')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('#hotInstance', { timeout: 30000 })
|
|
||||||
.find('div.ht_master.handsontable')
|
|
||||||
.find('div.wtHolder')
|
|
||||||
.find('div.wtHider')
|
|
||||||
.find('div.wtSpreader')
|
|
||||||
.find('table.htCore')
|
|
||||||
.find('tbody')
|
|
||||||
.should((data) => {
|
|
||||||
let allEmpty = true
|
|
||||||
|
|
||||||
for (let col = 0; col < data[0].children.length; col++) {
|
|
||||||
const cell: any = data[0].children[col].children[5]
|
|
||||||
|
|
||||||
if (cell.innerText !== '') {
|
|
||||||
allEmpty = false
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allEmpty) done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('19 | Uploads Excel with data on random sheet surrounded with all empty cells', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('surrounded_data_all_cells_empty_excel.xlsx', () => {
|
|
||||||
checkResultOfXLSUpload(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('20 | Uploads Excel with data surrounded with empty cells ', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('surrounded_data_empty_cells_excel.xlsx', () => {
|
|
||||||
checkResultOfXLSUpload(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('21 | Uploads regular Excel file with first row marked for Delete (yes)', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('regular_excel_with_delete.xlsx', () => {
|
|
||||||
cy.get('.btn-upload-preview', { timeout: 60000 })
|
|
||||||
.should('be.visible')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('#hotInstance', { timeout: 30000 })
|
|
||||||
.find('div.ht_master.handsontable')
|
|
||||||
.find('div.wtHolder')
|
|
||||||
.find('div.wtHider')
|
|
||||||
.find('div.wtSpreader')
|
|
||||||
.find('table.htCore')
|
|
||||||
.find('tbody')
|
|
||||||
.should((data: JQuery<HTMLTableSectionElement>) => {
|
|
||||||
const firstRowFirstCol: Partial<HTMLElement> =
|
|
||||||
data[0].children[0].children[1]
|
|
||||||
|
|
||||||
if (
|
|
||||||
firstRowFirstCol.innerText &&
|
|
||||||
!firstRowFirstCol.innerText.toLowerCase().includes('yes')
|
|
||||||
) {
|
|
||||||
done('Delete? column from file not applied')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
submitExcel()
|
|
||||||
rejectExcel(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Large files break Cypress
|
|
||||||
|
|
||||||
// it ('? | Uploads Excel with size of 5MB', (done) => {
|
|
||||||
// attachExcelFile('5mb_excel.xlsx', () => {
|
|
||||||
// submitExcel();
|
|
||||||
// rejectExcel(done);
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
|
|
||||||
// it ('? | Uploads Excel with size of 15MB', (done) => {
|
|
||||||
// attachExcelFile('15mb_excel.xlsx', () => {
|
|
||||||
// submitExcel();
|
|
||||||
// rejectExcel(done);
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
|
|
||||||
//Large files tests end
|
|
||||||
|
|
||||||
this.afterEach(() => {
|
|
||||||
colorLog(`TEST END -------------`, '#3498DB')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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 viyaLib
|
|
||||||
|
|
||||||
for (let node of treeNodes) {
|
|
||||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
|
||||||
viyaLib = node
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.get(viyaLib).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 attachExcelFile = (excelFilename: string, callback?: any) => {
|
|
||||||
cy.get('.buttonBar button:last-child')
|
|
||||||
.should('exist')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('input[type="file"]#file-upload')
|
|
||||||
.attachFile(`/${fixturePath}/${excelFilename}`)
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.clr-abort-modal .modal-title').then((modalTitle) => {
|
|
||||||
if (!modalTitle[0].innerHTML.includes('Abort Message')) {
|
|
||||||
cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => {
|
|
||||||
modalBtn.click()
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (callback) callback()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitExcel = (callback?: any) => {
|
|
||||||
cy.get('.buttonBar button.preview-submit', { timeout: longerCommandTimeout })
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const rejectExcel = (callback?: any) => {
|
|
||||||
cy.get('button', { timeout: longerCommandTimeout })
|
|
||||||
.should('contain', 'Approve')
|
|
||||||
.then((allButtons: any) => {
|
|
||||||
for (let approvalButton of allButtons) {
|
|
||||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
|
||||||
approvalButton.click()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.get('button.btn-danger')
|
|
||||||
.should('exist')
|
|
||||||
.should('not.be.disabled')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.modal-footer button.btn-success-outline')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('app-history')
|
|
||||||
.should('exist')
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const acceptExcel = (callback?: any) => {
|
|
||||||
cy.get('button', { timeout: longerCommandTimeout })
|
|
||||||
.should('contain', 'Approve')
|
|
||||||
.then((allButtons: any) => {
|
|
||||||
for (let approvalButton of allButtons) {
|
|
||||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
|
||||||
approvalButton.click()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.get('#acceptBtn')
|
|
||||||
.should('exist')
|
|
||||||
.should('not.be.disabled')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) {
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkResultOfFormulaUpload = (callback?: any) => {
|
|
||||||
cy.get('#hotInstance', { timeout: longerCommandTimeout })
|
|
||||||
.find('div.ht_master.handsontable')
|
|
||||||
.find('div.wtHolder')
|
|
||||||
.find('div.wtHider')
|
|
||||||
.find('div.wtSpreader')
|
|
||||||
.find('table.htCore')
|
|
||||||
.find('tbody')
|
|
||||||
.should((data) => {
|
|
||||||
const cell: any = data[0].children[0].children[5]
|
|
||||||
expect(cell.innerText).to.equal('=1+1')
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkResultOfXLSUpload = (callback?: any) => {
|
|
||||||
cy.viewport(1280, 720)
|
|
||||||
cy.get('#hotInstance', { timeout: 30000 })
|
|
||||||
.find('div.ht_master.handsontable')
|
|
||||||
.find('div.wtHolder')
|
|
||||||
.find('div.wtHider')
|
|
||||||
.find('div.wtSpreader')
|
|
||||||
.find('table.htCore')
|
|
||||||
.find('tbody')
|
|
||||||
.should((data) => {
|
|
||||||
let cell: any = data[0].children[0].children[2]
|
|
||||||
|
|
||||||
expect(cell.innerText).to.equal('0')
|
|
||||||
cell = data[0].children[0].children[3]
|
|
||||||
expect(cell.innerText).to.equal('this is dummy data changed in excel')
|
|
||||||
cell = data[0].children[0].children[4]
|
|
||||||
expect(cell.innerText).to.equal('▼\nOption 1')
|
|
||||||
cell = data[0].children[0].children[5]
|
|
||||||
expect(cell.innerText).to.equal('42')
|
|
||||||
cell = data[0].children[0].children[6]
|
|
||||||
expect(cell.innerText).to.equal('▼\n1960-02-12')
|
|
||||||
cell = data[0].children[0].children[7]
|
|
||||||
expect(cell.innerText).to.equal('▼\n1960-01-01 00:00:42')
|
|
||||||
cell = data[0].children[0].children[8]
|
|
||||||
expect(cell.innerText).to.equal('00:00:42')
|
|
||||||
cell = data[0].children[0].children[9]
|
|
||||||
expect(cell.innerText).to.equal('3')
|
|
||||||
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.get('#hotInstance', { timeout: 30000 })
|
|
||||||
.find('div.ht_master.handsontable')
|
|
||||||
.find('div.wtHolder')
|
|
||||||
.scrollTo('right')
|
|
||||||
.find('div.wtHider')
|
|
||||||
.find('div.wtSpreader')
|
|
||||||
.find('table.htCore')
|
|
||||||
.find('tbody')
|
|
||||||
.should((data) => {
|
|
||||||
let cell: any = data[0].children[0].children[1]
|
|
||||||
|
|
||||||
cell = data[0].children[0].children[9]
|
|
||||||
|
|
||||||
expect(cell.innerText).to.equal('44')
|
|
||||||
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const visitPage = (url: string) => {
|
|
||||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorLog = (msg: string, color: string) => {
|
|
||||||
console.log('%c' + msg, 'color:' + color + ';font-weight:bold;')
|
|
||||||
}
|
|
||||||
@@ -1,376 +0,0 @@
|
|||||||
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 = 'excels_general/'
|
|
||||||
|
|
||||||
context('filtering tests: ', function () {
|
|
||||||
this.beforeAll(() => {
|
|
||||||
cy.visit(`${hostUrl}/SASLogon/logout`, { timeout: longerCommandTimeout })
|
|
||||||
cy.loginAndUpdateValidKey()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.beforeEach(() => {
|
|
||||||
cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout })
|
|
||||||
|
|
||||||
visitPage('home')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('1 | filter char field', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openFilterPopup(() => {
|
|
||||||
setFilterWithValue('SOME_CHAR', 'this is dummy data', 'value', () => {
|
|
||||||
checkInfoBarIncludes(
|
|
||||||
`AND,AND,0,SOME_CHAR,=,"'this is dummy data'"`,
|
|
||||||
(includes: boolean) => {
|
|
||||||
if (includes) done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('2 | filter number field', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openFilterPopup(() => {
|
|
||||||
setFilterWithValue('SOME_NUM', '42', 'value', () => {
|
|
||||||
checkInfoBarIncludes(`AND,AND,0,SOME_NUM,=,42`, (includes: boolean) => {
|
|
||||||
if (includes) done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it.only('3 | filter time field', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openFilterPopup(() => {
|
|
||||||
setFilterWithValue('SOME_TIME', '00:00:42', 'time', () => {
|
|
||||||
checkInfoBarIncludes(
|
|
||||||
`AND,AND,0,SOME_TIME,=,42`,
|
|
||||||
(includes: boolean) => {
|
|
||||||
if (includes) done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('3.1 | Non picker - filter time field', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openFilterPopup(() => {
|
|
||||||
setFilterWithValue('SOME_TIME', '42', 'value', () => {
|
|
||||||
checkInfoBarIncludes(
|
|
||||||
`AND,AND,0,SOME_TIME,=,42`,
|
|
||||||
(includes: boolean) => {
|
|
||||||
if (includes) done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('4 | filter date field', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openFilterPopup(() => {
|
|
||||||
setFilterWithValue('SOME_DATE', '12/02/1960', 'date', () => {
|
|
||||||
checkInfoBarIncludes(
|
|
||||||
`AND,AND,0,SOME_DATE,=,42`,
|
|
||||||
(includes: boolean) => {
|
|
||||||
if (includes) done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('4.1 | Non picker - filter date field', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openFilterPopup(() => {
|
|
||||||
setFilterWithValue('SOME_DATE', '42', 'value', () => {
|
|
||||||
checkInfoBarIncludes(
|
|
||||||
`AND,AND,0,SOME_DATE,=,42`,
|
|
||||||
(includes: boolean) => {
|
|
||||||
if (includes) done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('5 | filter datetime field', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openFilterPopup(() => {
|
|
||||||
setFilterWithValue(
|
|
||||||
'SOME_DATETIME',
|
|
||||||
'01/01/1960 00:00:42',
|
|
||||||
'datetime',
|
|
||||||
() => {
|
|
||||||
checkInfoBarIncludes(
|
|
||||||
`AND,AND,0,SOME_DATETIME,=,42`,
|
|
||||||
(includes: boolean) => {
|
|
||||||
if (includes) done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('5.1 | Non picker - filter datetime field', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openFilterPopup(() => {
|
|
||||||
setFilterWithValue('SOME_DATETIME', '42', 'value', () => {
|
|
||||||
checkInfoBarIncludes(
|
|
||||||
`AND,AND,0,SOME_DATETIME,=,42`,
|
|
||||||
(includes: boolean) => {
|
|
||||||
if (includes) done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('6 | filter date field IN', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openFilterPopup(() => {
|
|
||||||
setFilterWithValue('SOME_DATE', '', 'in', () => {
|
|
||||||
checkInfoBarIncludes(
|
|
||||||
`AND,AND,0,SOME_DATE,IN,(0)`,
|
|
||||||
(includes: boolean) => {
|
|
||||||
if (includes) done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('7 | filter bestnum field BETWEEN', (done) => {
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
openFilterPopup(() => {
|
|
||||||
setFilterWithValue('SOME_BESTNUM', '0-10', 'between', () => {
|
|
||||||
checkInfoBarIncludes(
|
|
||||||
`AND,AND,0,SOME_BESTNUM,BETWEEN,0 AND 10`,
|
|
||||||
(includes: boolean) => {
|
|
||||||
if (includes) done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const checkInfoBarIncludes = (text: string, callback: any) => {
|
|
||||||
cy.get('.infoBar b', { timeout: longerCommandTimeout }).then((el: any) => {
|
|
||||||
const includes = el[0].innerText.toLowerCase().includes(text.toLowerCase())
|
|
||||||
|
|
||||||
if (callback) callback(includes)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const openFilterPopup = (
|
|
||||||
callback?: any,
|
|
||||||
usePickers: boolean = true,
|
|
||||||
isViewerFiltering: boolean = false
|
|
||||||
) => {
|
|
||||||
const filterButton = isViewerFiltering
|
|
||||||
? '.btn-outline.filterSide'
|
|
||||||
: '.btnCtrl .btnView'
|
|
||||||
|
|
||||||
cy.get(filterButton, { timeout: longerCommandTimeout }).then(
|
|
||||||
(optionsButton: any) => {
|
|
||||||
optionsButton.click()
|
|
||||||
|
|
||||||
if (isViewerFiltering) {
|
|
||||||
cy.wait(300)
|
|
||||||
|
|
||||||
cy.get('.dropdown-menu button').then(async (dropdownButtons: any) => {
|
|
||||||
let filterButton = null
|
|
||||||
|
|
||||||
for (let btn of dropdownButtons) {
|
|
||||||
if (btn.innerText.toLowerCase().includes('filter')) {
|
|
||||||
filterButton = btn
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterButton) {
|
|
||||||
filterButton.click()
|
|
||||||
|
|
||||||
if (usePickers) turnOnPickers()
|
|
||||||
|
|
||||||
if (callback) callback()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (usePickers) turnOnPickers()
|
|
||||||
if (callback) callback()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const turnOnPickers = () => {
|
|
||||||
cy.get('#usePickers')
|
|
||||||
.should('exist')
|
|
||||||
.then((picker: any) => {
|
|
||||||
picker[0].click()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const setFilterWithValue = (
|
|
||||||
variableValue: string,
|
|
||||||
valueString: string,
|
|
||||||
valueField: 'value' | 'time' | 'date' | 'datetime' | 'in' | 'between',
|
|
||||||
callback?: any
|
|
||||||
) => {
|
|
||||||
cy.wait(600)
|
|
||||||
|
|
||||||
cy.focused().type(variableValue)
|
|
||||||
cy.wait(100)
|
|
||||||
// cy.focused().trigger('input')
|
|
||||||
cy.get('.variable-col .autocomplete-wrapper', { withinSubject: null })
|
|
||||||
.first()
|
|
||||||
.trigger('keydown', { key: 'ArrowDown' })
|
|
||||||
cy.get('.variable-col .autocomplete-wrapper', {
|
|
||||||
withinSubject: null
|
|
||||||
}).trigger('keydown', { key: 'Enter' })
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.wait(100)
|
|
||||||
|
|
||||||
if (valueField === 'in') {
|
|
||||||
cy.focused().select(valueField.toUpperCase()).trigger('change')
|
|
||||||
} else if (valueField === 'between') {
|
|
||||||
cy.focused().select(valueField.toUpperCase()).trigger('change')
|
|
||||||
} else {
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.wait(100)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (valueField) {
|
|
||||||
case 'value': {
|
|
||||||
cy.focused().type(valueString)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'time': {
|
|
||||||
cy.focused().type(valueString)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'date': {
|
|
||||||
cy.focused().type(valueString)
|
|
||||||
cy.focused().tab()
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'datetime': {
|
|
||||||
const date = valueString.split(' ')[0]
|
|
||||||
const time = valueString.split(' ')[1]
|
|
||||||
|
|
||||||
cy.focused().type(date)
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.focused().type(time)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'in': {
|
|
||||||
cy.get('.checkbox-vals').then(() => {
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.focused().click()
|
|
||||||
cy.get('.no-values')
|
|
||||||
.should('not.exist')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('clr-checkbox-wrapper input').then((inputs: any) => {
|
|
||||||
inputs[0].click()
|
|
||||||
cy.get('.in-values-modal .modal-footer button').click()
|
|
||||||
|
|
||||||
cy.get('.modal-footer .btn-success-outline').click()
|
|
||||||
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'between': {
|
|
||||||
cy.focused().tab()
|
|
||||||
|
|
||||||
const start = valueString.split('-')[0]
|
|
||||||
const end = valueString.split('-')[1]
|
|
||||||
|
|
||||||
cy.focused().type(start)
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.focused().type(end)
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.wait(100)
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.wait(100)
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.wait(100)
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.wait(100)
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.wait(100)
|
|
||||||
cy.focused().click()
|
|
||||||
|
|
||||||
if (callback) callback()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 viyaLib
|
|
||||||
|
|
||||||
for (let node of treeNodes) {
|
|
||||||
if (new RegExp(libNameIncludes).test(node.innerText.toLowerCase())) {
|
|
||||||
viyaLib = node
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.get(viyaLib).within(() => {
|
|
||||||
cy.get('.clr-tree-node-content-container p').click()
|
|
||||||
|
|
||||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
|
||||||
for (let innerNode of innerNodes) {
|
|
||||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
|
||||||
innerNode.click()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const visitPage = (url: string) => {
|
|
||||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
||||||
}
|
|
||||||
@@ -1,719 +0,0 @@
|
|||||||
import { arrayBufferToBase64 } from './../util/helper-functions'
|
|
||||||
import * as moment from 'moment'
|
|
||||||
|
|
||||||
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 fixturePath = 'excels_general/'
|
|
||||||
const serverType = Cypress.env('serverType')
|
|
||||||
const site_id = Cypress.env(`site_id_${serverType}`)
|
|
||||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
|
||||||
const testLicenceUserLimits = Cypress.env('testLicenceUserLimits')
|
|
||||||
|
|
||||||
/** IMPORTANT NOTICE
|
|
||||||
* Before running tests, make sure that table `MPE_USERS` is present
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface EditConfigTableCells {
|
|
||||||
varName: string
|
|
||||||
varValue: string
|
|
||||||
}
|
|
||||||
|
|
||||||
context('licensing tests: ', function () {
|
|
||||||
this.beforeAll(() => {
|
|
||||||
cy.loginAndUpdateValidKey()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.beforeEach(() => {
|
|
||||||
cy.visit(hostUrl + appLocation)
|
|
||||||
|
|
||||||
visitPage('home')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('1 | key valid, not expired', (done) => {
|
|
||||||
let keyData = {
|
|
||||||
valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
|
|
||||||
users_allowed: 4,
|
|
||||||
hot_license_key: '',
|
|
||||||
demo: false,
|
|
||||||
site_id: site_id
|
|
||||||
}
|
|
||||||
|
|
||||||
let keys: { licenseKey: any; activationKey: any }
|
|
||||||
generateKeys(keyData, (keysGen: any) => {
|
|
||||||
keys = keysGen
|
|
||||||
|
|
||||||
cy.wait(2000)
|
|
||||||
|
|
||||||
isLicensingPage((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
inputLicenseKeyPage(keys.licenseKey, keys.activationKey)
|
|
||||||
|
|
||||||
cy.wait(2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(10000)
|
|
||||||
}
|
|
||||||
|
|
||||||
visitPage('home')
|
|
||||||
|
|
||||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
|
||||||
timeout: longerCommandTimeout
|
|
||||||
}).then((treeNodes: any) => {
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('2 | Key will expire in less then 14 days, not free tier', (done) => {
|
|
||||||
// make 2 separate for this one
|
|
||||||
let keyData = {
|
|
||||||
valid_until: moment().add(10, 'day').format('YYYY-MM-DD'),
|
|
||||||
users_allowed: 4,
|
|
||||||
hot_license_key: '',
|
|
||||||
demo: false,
|
|
||||||
site_id: site_id
|
|
||||||
}
|
|
||||||
|
|
||||||
let keys: { licenseKey: any; activationKey: any }
|
|
||||||
generateKeys(keyData, (keysGen: any) => {
|
|
||||||
keys = keysGen
|
|
||||||
console.log('keys', keys)
|
|
||||||
|
|
||||||
cy.wait(2000)
|
|
||||||
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(20000)
|
|
||||||
}
|
|
||||||
updateLicenseKeyQuick(keysGen, () => {
|
|
||||||
cy.wait(2000)
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(20000)
|
|
||||||
}
|
|
||||||
verifyLicensingWarning('This license key will expire in ', () => {
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('3 | key expired, free tier works', (done) => {
|
|
||||||
let keyData = {
|
|
||||||
valid_until: moment().subtract(1, 'day').format('YYYY-MM-DD'),
|
|
||||||
users_allowed: 4,
|
|
||||||
hot_license_key: '',
|
|
||||||
demo: false,
|
|
||||||
site_id: site_id
|
|
||||||
}
|
|
||||||
|
|
||||||
let keys: { licenseKey: any; activationKey: any }
|
|
||||||
generateKeys(keyData, (keysGen: any) => {
|
|
||||||
keys = keysGen
|
|
||||||
|
|
||||||
cy.wait(2000)
|
|
||||||
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(20000)
|
|
||||||
}
|
|
||||||
cy.wait(2000)
|
|
||||||
updateLicenseKeyQuick(keysGen, () => {
|
|
||||||
cy.wait(2000)
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(20000)
|
|
||||||
}
|
|
||||||
verifyLicensingPage(
|
|
||||||
'Licence key is expired - please contact',
|
|
||||||
(success: boolean) => {
|
|
||||||
if (success) {
|
|
||||||
verifyLicensingWarning(
|
|
||||||
'(FREE Tier) - Problem with licence',
|
|
||||||
() => {
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('4 | key invalid, free tier works', (done) => {
|
|
||||||
let keyData = {
|
|
||||||
valid_until: moment().subtract(1, 'day').format('YYYY-MM-DD'),
|
|
||||||
users_allowed: 4,
|
|
||||||
hot_license_key: '',
|
|
||||||
demo: false,
|
|
||||||
site_id: site_id
|
|
||||||
}
|
|
||||||
|
|
||||||
let keys: { licenseKey: any; activationKey: any }
|
|
||||||
generateKeys(keyData, (keysGen: any) => {
|
|
||||||
keys = keysGen
|
|
||||||
keys.activationKey = 'invalid' + keys.activationKey
|
|
||||||
|
|
||||||
cy.wait(2000)
|
|
||||||
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(20000)
|
|
||||||
}
|
|
||||||
cy.wait(2000)
|
|
||||||
updateLicenseKeyQuick(keysGen, () => {
|
|
||||||
cy.wait(2000)
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(20000)
|
|
||||||
}
|
|
||||||
verifyLicensingPage(
|
|
||||||
'Licence key is invalid - please contact',
|
|
||||||
(success: boolean) => {
|
|
||||||
if (success) {
|
|
||||||
verifyLicensingWarning(
|
|
||||||
'(FREE Tier) - Problem with licence',
|
|
||||||
() => {
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('5 | key for wrong organisation, free tier works', (done) => {
|
|
||||||
let keyData = {
|
|
||||||
valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
|
|
||||||
users_allowed: 4,
|
|
||||||
hot_license_key: '',
|
|
||||||
demo: false,
|
|
||||||
site_id: 100
|
|
||||||
}
|
|
||||||
|
|
||||||
let keys: { licenseKey: any; activationKey: any }
|
|
||||||
generateKeys(keyData, (keysGen: any) => {
|
|
||||||
keys = keysGen
|
|
||||||
keys.activationKey = keys.activationKey
|
|
||||||
|
|
||||||
cy.wait(2000)
|
|
||||||
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(20000)
|
|
||||||
}
|
|
||||||
cy.wait(2000)
|
|
||||||
updateLicenseKeyQuick(keysGen, () => {
|
|
||||||
cy.wait(2000)
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(20000)
|
|
||||||
}
|
|
||||||
verifyLicensingPage(
|
|
||||||
'SYSSITE (below) is not found',
|
|
||||||
(success: boolean) => {
|
|
||||||
if (success) {
|
|
||||||
verifyLicensingWarning(
|
|
||||||
'(FREE Tier) - Problem with licence',
|
|
||||||
() => {
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (testLicenceUserLimits) {
|
|
||||||
it('4 | User try to register when limit is reached', (done) => {
|
|
||||||
let keyData = {
|
|
||||||
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
|
|
||||||
users_allowed: 10,
|
|
||||||
hot_license_key: '',
|
|
||||||
demo: false,
|
|
||||||
site_id: site_id
|
|
||||||
}
|
|
||||||
let keyData2 = {
|
|
||||||
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
|
|
||||||
users_allowed: 1,
|
|
||||||
hot_license_key: '',
|
|
||||||
demo: false,
|
|
||||||
site_id: site_id
|
|
||||||
}
|
|
||||||
generateKeys(keyData, (keysGen: any) => {
|
|
||||||
generateKeys(keyData2, (keysGen2: any) => {
|
|
||||||
cy.wait(2000)
|
|
||||||
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(20000)
|
|
||||||
}
|
|
||||||
updateLicenseKeyQuick(keysGen, () => {
|
|
||||||
cy.wait(2000)
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(20000)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLicenseKeyQuick(keysGen2, () => {
|
|
||||||
cy.wait(2000)
|
|
||||||
|
|
||||||
const random = Cypress._.random(0, 1000)
|
|
||||||
const newUser = {
|
|
||||||
username: `randomusername${random}notregistered`,
|
|
||||||
last_seen_at: moment().add(1, 'month').format('YYYY-MM-DD'),
|
|
||||||
registered_at: moment().add(1, 'month').format('YYYY-MM-DD')
|
|
||||||
}
|
|
||||||
updateUsersTable(
|
|
||||||
{ deleteAll: true, newUsers: [newUser] },
|
|
||||||
() => {
|
|
||||||
logout(() => {
|
|
||||||
cy.visit(hostUrl + appLocation)
|
|
||||||
// cy.get('input.username').type(username)
|
|
||||||
// cy.get('input.password').type(password)
|
|
||||||
// cy.get('.login-group button').click()
|
|
||||||
|
|
||||||
visitPage('home')
|
|
||||||
|
|
||||||
cy.wait(2000)
|
|
||||||
|
|
||||||
verifyLicensingPage(
|
|
||||||
'The registered number of users reached the limit specified for your licence.',
|
|
||||||
(success: boolean) => {
|
|
||||||
if (success) done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('5 | Show warning banner when limit is exceeded', (done) => {
|
|
||||||
let keyData = {
|
|
||||||
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
|
|
||||||
users_allowed: 10,
|
|
||||||
hot_license_key: '',
|
|
||||||
demo: false,
|
|
||||||
site_id: site_id
|
|
||||||
}
|
|
||||||
let keyData2 = {
|
|
||||||
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
|
|
||||||
users_allowed: 1,
|
|
||||||
hot_license_key: '',
|
|
||||||
demo: false,
|
|
||||||
site_id: site_id
|
|
||||||
}
|
|
||||||
|
|
||||||
generateKeys(keyData, (keysGen: any) => {
|
|
||||||
generateKeys(keyData2, (keysGen2: any) => {
|
|
||||||
cy.wait(2000)
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(20000)
|
|
||||||
}
|
|
||||||
updateLicenseKeyQuick(keysGen, () => {
|
|
||||||
cy.wait(2000)
|
|
||||||
acceptTermsIfPresented((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.wait(20000)
|
|
||||||
}
|
|
||||||
const random = Cypress._.random(0, 1000)
|
|
||||||
const newUser = {
|
|
||||||
username: `randomusername${random}`,
|
|
||||||
last_seen_at: moment().add(1, 'month').format('YYYY-MM-DD'),
|
|
||||||
registered_at: moment().add(1, 'month').format('YYYY-MM-DD')
|
|
||||||
}
|
|
||||||
updateUsersTable(
|
|
||||||
{ deleteAll: true, keep: username, newUsers: [newUser] },
|
|
||||||
() => {
|
|
||||||
updateLicenseKeyQuick(keysGen2, () => {
|
|
||||||
cy.wait(2000)
|
|
||||||
verifyLicensingWarning(
|
|
||||||
'The registered number of users exceeds the limit specified for your license.',
|
|
||||||
() => {
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const logout = (callback?: any) => {
|
|
||||||
cy.get('.header-actions .dropdown-toggle')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.header-actions .dropdown-menu > .separator')
|
|
||||||
.next()
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const acceptTermsIfPresented = (callback?: any) => {
|
|
||||||
cy.url().then((url: string) => {
|
|
||||||
if (url.includes('licensing/register')) {
|
|
||||||
cy.get('.card-block')
|
|
||||||
.scrollTo('bottom')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('#checkbox1')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (callback) callback(false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const isLicensingPage = (callback: any) => {
|
|
||||||
return cy.url().then((url: string) => {
|
|
||||||
callback(
|
|
||||||
url.includes('#/licensing/') && !url.includes('licensing/register')
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyLicensingPage = (text: string, callback: any) => {
|
|
||||||
// visitPage('home')
|
|
||||||
|
|
||||||
cy.wait(1000)
|
|
||||||
isLicensingPage((result: boolean) => {
|
|
||||||
if (result) {
|
|
||||||
cy.get('p.key-error')
|
|
||||||
.should('contain', text)
|
|
||||||
.then((treeNodes: any) => {
|
|
||||||
callback(true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyLicensingWarning = (text: string, callback: any) => {
|
|
||||||
visitPage('home')
|
|
||||||
|
|
||||||
cy.wait(1000)
|
|
||||||
cy.get("div[role='alert'] .alert-text")
|
|
||||||
.invoke('text')
|
|
||||||
.should('contain', text)
|
|
||||||
.then(() => {
|
|
||||||
callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputLicenseKeyPage = (licenseKey: string, activationKey: string) => {
|
|
||||||
cy.get('button').contains('Paste licence').click()
|
|
||||||
|
|
||||||
cy.get('.license-key-form textarea', { timeout: longerCommandTimeout })
|
|
||||||
.invoke('val', licenseKey)
|
|
||||||
.trigger('input')
|
|
||||||
.should('not.be.undefined')
|
|
||||||
cy.get('.activation-key-form textarea', { timeout: longerCommandTimeout })
|
|
||||||
.invoke('val', activationKey)
|
|
||||||
.trigger('input')
|
|
||||||
.should('not.be.undefined')
|
|
||||||
cy.get('button.apply-keys').click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateUsersTable = (options: any, callback?: any) => {
|
|
||||||
visitPage('home')
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_users')
|
|
||||||
|
|
||||||
clickOnEdit(() => {
|
|
||||||
cy.get('.ht_master tbody tr').then((rows: any) => {
|
|
||||||
if (options.deleteAll) {
|
|
||||||
for (let row of rows) {
|
|
||||||
const user_id = row.childNodes[2]
|
|
||||||
if (!options.keep || user_id.innerText !== options.keep) {
|
|
||||||
cy.get(row.childNodes[1])
|
|
||||||
.dblclick()
|
|
||||||
.then(() => {
|
|
||||||
cy.focused().type('{selectall}').type('Yes').type('{enter}')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (options.newUsers && options.newUsers.length) {
|
|
||||||
for (let newUser of options.newUsers) {
|
|
||||||
clickOnAddRow(() => {
|
|
||||||
cy.get('#hotInstance tbody tr:last-child').then((rows: any) => {
|
|
||||||
cy.get(rows[0].childNodes[2])
|
|
||||||
.dblclick()
|
|
||||||
.then(() => {
|
|
||||||
cy.focused()
|
|
||||||
.type('{selectall}')
|
|
||||||
.type(newUser.username)
|
|
||||||
.type('{enter}')
|
|
||||||
})
|
|
||||||
// cy.get(rows[0].childNodes[3])
|
|
||||||
// .dblclick()
|
|
||||||
// .then(() => {
|
|
||||||
// cy.focused()
|
|
||||||
// .type('{selectall}')
|
|
||||||
// .type(newUser.last_seen_at)
|
|
||||||
// .type('{enter}')
|
|
||||||
// })
|
|
||||||
// cy.get(rows[0].childNodes[4])
|
|
||||||
// .dblclick()
|
|
||||||
// .then(() => {
|
|
||||||
// cy.focused()
|
|
||||||
// .type('{selectall}')
|
|
||||||
// .type(newUser.registered_at)
|
|
||||||
// .type('{enter}')
|
|
||||||
// })
|
|
||||||
submitTable(() => {
|
|
||||||
cy.wait(2000)
|
|
||||||
approveTable(callback)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeLicenseKeyTable = (keys: any, callback?: any) => {
|
|
||||||
visitPage('home')
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_config')
|
|
||||||
|
|
||||||
clickOnEdit(() => {
|
|
||||||
editTableField(
|
|
||||||
[
|
|
||||||
{ varName: 'DC_ACTIVATION_KEY', varValue: keys.activationKey },
|
|
||||||
{ varName: 'DC_LICENCE_KEY', varValue: keys.licenseKey }
|
|
||||||
],
|
|
||||||
() => {
|
|
||||||
submitTable(() => {
|
|
||||||
cy.wait(2000)
|
|
||||||
approveTable(() => {
|
|
||||||
cy.reload()
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateLicenseKeyQuick = (keys: any, callback: any) => {
|
|
||||||
isLicensingPage((result: boolean) => {
|
|
||||||
if (!result) {
|
|
||||||
visitPage('licensing/update')
|
|
||||||
cy.wait(2000)
|
|
||||||
}
|
|
||||||
inputLicenseKeyPage(keys.licenseKey, keys.activationKey)
|
|
||||||
|
|
||||||
callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateKeys = async (licenseData: any, resultCallback?: any) => {
|
|
||||||
let keyPair = await window.crypto.subtle.generateKey(
|
|
||||||
{
|
|
||||||
name: 'RSA-OAEP',
|
|
||||||
modulusLength: 2024,
|
|
||||||
publicExponent: new Uint8Array([1, 0, 1]),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['encrypt', 'decrypt']
|
|
||||||
)
|
|
||||||
|
|
||||||
const encoded = new TextEncoder().encode(JSON.stringify(licenseData))
|
|
||||||
|
|
||||||
const cipher = await window.crypto.subtle
|
|
||||||
.encrypt(
|
|
||||||
{
|
|
||||||
name: 'RSA-OAEP'
|
|
||||||
},
|
|
||||||
keyPair.publicKey,
|
|
||||||
encoded
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
(value) => {
|
|
||||||
return value
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
console.log('Encrpyt error', err)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!cipher) {
|
|
||||||
alert('Encryptin keys failed')
|
|
||||||
throw new Error('Encryptin keys failed')
|
|
||||||
}
|
|
||||||
|
|
||||||
const privateKeyBytes = await window.crypto.subtle.exportKey(
|
|
||||||
'pkcs8',
|
|
||||||
keyPair.privateKey
|
|
||||||
)
|
|
||||||
|
|
||||||
const activationKey = await arrayBufferToBase64(privateKeyBytes)
|
|
||||||
const licenseKey = await arrayBufferToBase64(cipher)
|
|
||||||
|
|
||||||
if (resultCallback)
|
|
||||||
resultCallback({
|
|
||||||
activationKey,
|
|
||||||
licenseKey
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const editTableField = (edits: EditConfigTableCells[], callback?: any) => {
|
|
||||||
cy.get('td').then((tdNodes: any) => {
|
|
||||||
for (let edit of edits) {
|
|
||||||
let correctRow = false
|
|
||||||
|
|
||||||
for (let node of tdNodes) {
|
|
||||||
if (correctRow) {
|
|
||||||
cy.get(node)
|
|
||||||
.dblclick()
|
|
||||||
.then(() => {
|
|
||||||
// textarea update on long keys
|
|
||||||
cy.focused().invoke('val', edit.varValue).type('{enter}')
|
|
||||||
})
|
|
||||||
|
|
||||||
correctRow = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.innerText.includes(edit.varName)) {
|
|
||||||
correctRow = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const openTableFromTree = (
|
|
||||||
libNameIncludes: string,
|
|
||||||
tablename: string,
|
|
||||||
callback?: any
|
|
||||||
) => {
|
|
||||||
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 viyaLib
|
|
||||||
|
|
||||||
for (let node of treeNodes) {
|
|
||||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
|
||||||
viyaLib = node
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.get(viyaLib).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()
|
|
||||||
if (callback) callback()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const clickOnAddRow = (callback?: any) => {
|
|
||||||
cy.get('.btnCtrl button.btn-success')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const clickOnEdit = (callback?: any) => {
|
|
||||||
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitTable = (callback?: any) => {
|
|
||||||
cy.get('.btnCtrl button.btn-primary', { timout: longerCommandTimeout })
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get(".modal.ng-star-inserted button[type='submit']")
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const approveTable = (callback?: any) => {
|
|
||||||
cy.get('button', { timeout: longerCommandTimeout })
|
|
||||||
.should('contain', 'Approve')
|
|
||||||
.then((allButtons: any) => {
|
|
||||||
for (let approvalButton of allButtons) {
|
|
||||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
|
||||||
approvalButton.click()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.get('button#acceptBtn', { timeout: longerCommandTimeout })
|
|
||||||
.should('exist')
|
|
||||||
.should('not.be.disabled')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('app-history', { timeout: longerCommandTimeout })
|
|
||||||
.should('exist')
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const visitPage = (url: string) => {
|
|
||||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
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 = 'excels/'
|
|
||||||
|
|
||||||
context('liveness tests: ', function () {
|
|
||||||
this.beforeAll(() => {
|
|
||||||
if (serverType !== 'SASJS') {
|
|
||||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
|
||||||
}
|
|
||||||
cy.loginAndUpdateValidKey(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.beforeEach(() => {
|
|
||||||
cy.visit(hostUrl + appLocation)
|
|
||||||
|
|
||||||
visitPage('home')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('1 | Login and submit test', (done) => {
|
|
||||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
|
||||||
timeout: longerCommandTimeout
|
|
||||||
}).then((treeNodes: any) => {
|
|
||||||
libraryExistsInTree('viya', treeNodes)
|
|
||||||
? openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
: openTableFromTree('dc', 'mpe_x_test')
|
|
||||||
|
|
||||||
attachExcelFile('regular_excel.xlsx', () => {
|
|
||||||
submitExcel()
|
|
||||||
rejectExcel(done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thist part will be needed if we add more tests in future
|
|
||||||
*/
|
|
||||||
// this.afterEach(() => {
|
|
||||||
// cy.visit('https://sas.4gl.io/SASLogon/logout');
|
|
||||||
// })
|
|
||||||
})
|
|
||||||
|
|
||||||
const visitPage = (url: string) => {
|
|
||||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const libraryExistsInTree = (libName: string, nodes: any) => {
|
|
||||||
for (let node of nodes) {
|
|
||||||
if (node.innerText.toLowerCase().includes(libName.toLowerCase()))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const openTableFromTree = (
|
|
||||||
libNameIncludes: string,
|
|
||||||
tablename: string,
|
|
||||||
finish: any
|
|
||||||
) => {
|
|
||||||
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 viyaLib
|
|
||||||
|
|
||||||
for (let node of treeNodes) {
|
|
||||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
|
||||||
viyaLib = node
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!viyaLib && finish) finish(false)
|
|
||||||
|
|
||||||
cy.get(viyaLib).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()
|
|
||||||
if (finish) finish(true)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachExcelFile = (excelFilename: string, callback?: any) => {
|
|
||||||
cy.get('.buttonBar button:last-child')
|
|
||||||
.should('exist')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('input[type="file"]#file-upload').attachFile(
|
|
||||||
`/${fixturePath}/${excelFilename}`
|
|
||||||
)
|
|
||||||
cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => {
|
|
||||||
modalBtn.click()
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitExcel = (callback?: any) => {
|
|
||||||
cy.get('.buttonBar button.preview-submit', { timeout: longerCommandTimeout })
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const rejectExcel = (callback?: any) => {
|
|
||||||
cy.get('button', { timeout: longerCommandTimeout })
|
|
||||||
.should('contain', 'Approve')
|
|
||||||
.then((allButtons: any) => {
|
|
||||||
for (let approvalButton of allButtons) {
|
|
||||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
|
||||||
approvalButton.click()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.get('button.btn-danger')
|
|
||||||
.should('exist')
|
|
||||||
.should('not.be.disabled')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.modal-footer button.btn-success-outline')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('app-history')
|
|
||||||
.should('exist')
|
|
||||||
.then(() => {
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
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 = 'excels_general/'
|
|
||||||
|
|
||||||
context('metanav tests: ', function () {
|
|
||||||
this.beforeAll(() => {
|
|
||||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
|
||||||
cy.loginAndUpdateValidKey()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.beforeEach(() => {
|
|
||||||
cy.visit(hostUrl + appLocation)
|
|
||||||
cy.get('input.username').type(username)
|
|
||||||
cy.get('input.password').type(password)
|
|
||||||
cy.get('.login-group button').click()
|
|
||||||
|
|
||||||
visitPage('view/metadata')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('1 | Opens metadata object', (done) => {
|
|
||||||
openFirstMetadataFromTree(() => {
|
|
||||||
// BLOCKER
|
|
||||||
// For unkown reasons, .clr-accordion-header-button always null although it is present on the page.
|
|
||||||
cy.get('.clr-accordion-header-button').then((panelNodes: any) => {
|
|
||||||
panelNodes[0].querySelector('button').click()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.afterEach(() => {
|
|
||||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const openFirstMetadataFromTree = (callback?: any) => {
|
|
||||||
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 firstMetaNode
|
|
||||||
|
|
||||||
firstMetaNode = treeNodes[1]
|
|
||||||
|
|
||||||
cy.get(firstMetaNode).within(() => {
|
|
||||||
cy.get('.clr-treenode-content').click()
|
|
||||||
callback()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const visitPage = (url: string) => {
|
|
||||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
||||||
}
|
|
||||||
@@ -1,624 +0,0 @@
|
|||||||
import { cloneDeep } from 'lodash-es'
|
|
||||||
|
|
||||||
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 = 'excels_general/'
|
|
||||||
|
|
||||||
context('editor tests: ', function () {
|
|
||||||
this.beforeAll(() => {
|
|
||||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
|
||||||
cy.loginAndUpdateValidKey()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.beforeEach(() => {
|
|
||||||
cy.visit(hostUrl + appLocation)
|
|
||||||
cy.wait(2000)
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
visitPage('home')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('1 | Add one viewbox', (done) => {
|
|
||||||
const viewbox_table = 'mpe_audit'
|
|
||||||
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
cy.get('.viewbox-open').click()
|
|
||||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
|
||||||
|
|
||||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
|
||||||
for (let viewboxNode of viewboxNodes) {
|
|
||||||
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
checkColumns(columns, () => {
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('2 | Add two viewboxes', (done) => {
|
|
||||||
const viewboxes = [
|
|
||||||
{
|
|
||||||
viewbox_table: 'mpe_audit',
|
|
||||||
columns: ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
viewbox_table: 'mpe_alerts',
|
|
||||||
columns: [
|
|
||||||
'TX_FROM',
|
|
||||||
'ALERT_EVENT',
|
|
||||||
'ALERT_LIB',
|
|
||||||
'ALERT_DS',
|
|
||||||
'ALERT_USER'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
cy.get('.viewbox-open').click()
|
|
||||||
openTableFromViewboxTree(
|
|
||||||
libraryToOpenIncludes,
|
|
||||||
viewboxes.map((viewbox) => viewbox.viewbox_table)
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
|
||||||
let found = 0
|
|
||||||
|
|
||||||
for (let viewboxNode of viewboxNodes) {
|
|
||||||
for (let viewbox of viewboxes) {
|
|
||||||
if (
|
|
||||||
viewboxNode.innerText.toLowerCase().includes(viewbox.viewbox_table)
|
|
||||||
)
|
|
||||||
found++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found < viewboxes.length) return
|
|
||||||
|
|
||||||
cy.get('.viewboxes-container .viewbox').then((viewboxNodes: any) => {
|
|
||||||
for (let viewboxNode of viewboxNodes) {
|
|
||||||
cy.get(viewboxNode).within(() => {
|
|
||||||
cy.get('.table-title').then((tableTitle) => {
|
|
||||||
const title = tableTitle[0].innerText
|
|
||||||
const viewbox = viewboxes.find((vb) =>
|
|
||||||
title.toLowerCase().includes(vb.viewbox_table)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (viewbox) {
|
|
||||||
cy.get('.ht_master.handsontable .htCore thead tr').then(
|
|
||||||
(viewboxColNodes: any) => {
|
|
||||||
let allColsHtml = viewboxColNodes[0].innerHTML
|
|
||||||
|
|
||||||
for (let col of viewbox?.columns) {
|
|
||||||
if (!allColsHtml.includes(col)) return
|
|
||||||
}
|
|
||||||
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('3 | Add viewbox, add columns', (done) => {
|
|
||||||
const viewbox_table = 'mpe_audit'
|
|
||||||
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
|
||||||
const additionalColumns = ['IS_PK']
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
cy.get('.viewbox-open').click()
|
|
||||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
|
||||||
|
|
||||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
|
||||||
for (let viewboxNode of viewboxNodes) {
|
|
||||||
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
openViewboxConfig(viewbox_table)
|
|
||||||
|
|
||||||
addColumns(additionalColumns)
|
|
||||||
|
|
||||||
checkColumns([...columns, ...additionalColumns], () => {
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('4 | Add viewbox, add columns and reorder', (done) => {
|
|
||||||
const viewbox_table = 'mpe_audit'
|
|
||||||
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
|
||||||
const additionalColumns = ['IS_PK', 'MOVE_TYPE']
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
cy.get('.viewbox-open').click()
|
|
||||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
|
||||||
|
|
||||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
|
||||||
for (let viewboxNode of viewboxNodes) {
|
|
||||||
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
openViewboxConfig(viewbox_table)
|
|
||||||
|
|
||||||
addColumns(additionalColumns, () => {
|
|
||||||
cy.wait(1000)
|
|
||||||
//reorder
|
|
||||||
cy.get('.col-box.column-MOVE_TYPE')
|
|
||||||
.realMouseDown({ button: 'left', position: 'center' })
|
|
||||||
.realMouseMove(0, 10, { position: 'center' })
|
|
||||||
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
|
||||||
cy.get('.col-box.column-IS_PK')
|
|
||||||
.realMouseMove(0, 0, { position: 'center' })
|
|
||||||
.realMouseUp()
|
|
||||||
//reorder end
|
|
||||||
|
|
||||||
cy.wait(500)
|
|
||||||
|
|
||||||
checkColumns([...columns, ...additionalColumns.reverse()], () => {
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('5 | Add viewbox, add columns, reorder, remove column, add again', (done) => {
|
|
||||||
const viewbox_table = 'mpe_audit'
|
|
||||||
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
|
||||||
const additionalColumns = ['IS_PK', 'MOVE_TYPE']
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
cy.get('.viewbox-open').click()
|
|
||||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
|
||||||
|
|
||||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
|
||||||
for (let viewboxNode of viewboxNodes) {
|
|
||||||
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
viewboxNode.click()
|
|
||||||
|
|
||||||
addColumns(additionalColumns, () => {
|
|
||||||
cy.wait(1000)
|
|
||||||
//reorder
|
|
||||||
cy.get('.col-box.column-MOVE_TYPE')
|
|
||||||
.realMouseDown({ button: 'left', position: 'center' })
|
|
||||||
.realMouseMove(0, 10, { position: 'center' })
|
|
||||||
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
|
||||||
cy.get('.col-box.column-IS_PK')
|
|
||||||
.realMouseMove(0, 0, { position: 'center' })
|
|
||||||
.realMouseUp()
|
|
||||||
//reorder end
|
|
||||||
|
|
||||||
cy.wait(500)
|
|
||||||
|
|
||||||
checkColumns([...columns, ...additionalColumns.reverse()], () => {
|
|
||||||
const colToRemove = 'MOVE_TYPE'
|
|
||||||
|
|
||||||
removeColumn(colToRemove)
|
|
||||||
checkColumns(
|
|
||||||
[
|
|
||||||
...columns,
|
|
||||||
...additionalColumns.filter((col) => col !== colToRemove)
|
|
||||||
],
|
|
||||||
() => {
|
|
||||||
addColumns([colToRemove], () => {
|
|
||||||
checkColumns(
|
|
||||||
[...columns, ...additionalColumns.reverse()],
|
|
||||||
() => {
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('6 | Add viewboxes, reload and check url restored configuration', (done) => {
|
|
||||||
const viewboxes = [
|
|
||||||
{
|
|
||||||
viewbox_table: 'mpe_audit',
|
|
||||||
columns: ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM'],
|
|
||||||
additionalColumns: ['IS_PK', 'MOVE_TYPE']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
viewbox_table: 'mpe_alerts',
|
|
||||||
columns: [
|
|
||||||
'TX_FROM',
|
|
||||||
'ALERT_EVENT',
|
|
||||||
'ALERT_LIB',
|
|
||||||
'ALERT_DS',
|
|
||||||
'ALERT_USER'
|
|
||||||
],
|
|
||||||
additionalColumns: ['TX_TO']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
cy.get('.viewbox-open').click()
|
|
||||||
openTableFromViewboxTree(libraryToOpenIncludes, [
|
|
||||||
viewboxes[0].viewbox_table,
|
|
||||||
viewboxes[1].viewbox_table
|
|
||||||
])
|
|
||||||
|
|
||||||
openViewboxConfig(viewboxes[0].viewbox_table)
|
|
||||||
|
|
||||||
cy.wait(500)
|
|
||||||
|
|
||||||
addColumns(viewboxes[0].additionalColumns, () => {
|
|
||||||
cy.wait(1000)
|
|
||||||
|
|
||||||
if (viewboxes[0].viewbox_table === 'mpe_audit') {
|
|
||||||
cy.get('.col-box.column-MOVE_TYPE')
|
|
||||||
.realMouseDown({ button: 'left', position: 'center' })
|
|
||||||
.realMouseMove(0, 10, { position: 'center' })
|
|
||||||
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
|
||||||
cy.get('.col-box.column-IS_PK')
|
|
||||||
.realMouseMove(0, 0, { position: 'center' })
|
|
||||||
.realMouseUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.wait(1000)
|
|
||||||
|
|
||||||
openViewboxConfig(viewboxes[1].viewbox_table)
|
|
||||||
addColumns(viewboxes[1].additionalColumns, () => {
|
|
||||||
cy.wait(1000).reload()
|
|
||||||
|
|
||||||
let result = 0
|
|
||||||
|
|
||||||
checkColumns(
|
|
||||||
[
|
|
||||||
...viewboxes[0].columns,
|
|
||||||
...cloneDeep(viewboxes[0].additionalColumns.reverse())
|
|
||||||
],
|
|
||||||
() => {
|
|
||||||
result++
|
|
||||||
|
|
||||||
if (result === 2) done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
checkColumns(
|
|
||||||
[...viewboxes[1].columns, ...viewboxes[1].additionalColumns],
|
|
||||||
() => {
|
|
||||||
result++
|
|
||||||
|
|
||||||
if (result === 2) done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('7 | Add viewboxes and filter', () => {
|
|
||||||
const viewboxes = ['mpe_x_test', 'mpe_validations']
|
|
||||||
|
|
||||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
|
||||||
|
|
||||||
cy.get('.viewbox-open').click()
|
|
||||||
openTableFromViewboxTree(libraryToOpenIncludes, viewboxes)
|
|
||||||
|
|
||||||
cy.wait(1000)
|
|
||||||
|
|
||||||
closeViewboxModal()
|
|
||||||
|
|
||||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
|
|
||||||
(viewboxNodes: any) => {
|
|
||||||
for (let viewboxNode of viewboxNodes) {
|
|
||||||
cy.get(viewboxNode).within(() => {
|
|
||||||
cy.get('.table-title').then((title: any) => {
|
|
||||||
cy.get('.hot-spinner')
|
|
||||||
.should('not.exist')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('clr-icon[shape="filter"]').then((filterButton) => {
|
|
||||||
filterButton[0].click()
|
|
||||||
})
|
|
||||||
|
|
||||||
if (title[0].innerText.includes('MPE_X_TEST')) {
|
|
||||||
setFilterWithValue(
|
|
||||||
'SOME_CHAR',
|
|
||||||
'this is dummy data',
|
|
||||||
'value',
|
|
||||||
() => {
|
|
||||||
cy.get('app-query', { withinSubject: null })
|
|
||||||
.should('not.exist')
|
|
||||||
.get('.ht_master.handsontable tbody tr')
|
|
||||||
.then((rowNodes) => {
|
|
||||||
const tr = rowNodes[0]
|
|
||||||
|
|
||||||
expect(rowNodes).to.have.length(1)
|
|
||||||
expect(tr.innerText).to.equal('0')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else if (title[0].innerText.includes('MPE_VALIDATIONS')) {
|
|
||||||
setFilterWithValue('BASE_COL', 'ALERT_LIB', 'value', () => {
|
|
||||||
cy.get('app-query', { withinSubject: null })
|
|
||||||
.should('not.exist')
|
|
||||||
.get('.ht_master.handsontable tbody tr')
|
|
||||||
.then((rowNodes) => {
|
|
||||||
const tr = rowNodes[0]
|
|
||||||
|
|
||||||
expect(rowNodes).to.have.length(1)
|
|
||||||
expect(tr.innerText).to.contain('ALERT_LIB')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const checkColumns = (columns: string[], callback: () => void) => {
|
|
||||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
|
|
||||||
(viewboxNodes: any) => {
|
|
||||||
for (let viewboxNode of viewboxNodes) {
|
|
||||||
cy.get(viewboxNode).within(() => {
|
|
||||||
cy.get('.ht_master.handsontable thead tr th').then(
|
|
||||||
(viewboxColNodes: any) => {
|
|
||||||
for (let i = 0; i < viewboxColNodes.length; i++) {
|
|
||||||
const col = columns[i]
|
|
||||||
const colNode = viewboxColNodes[i]
|
|
||||||
|
|
||||||
if (
|
|
||||||
!colNode.innerHTML.toLowerCase().includes(col.toLowerCase())
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeViewboxModal = () => {
|
|
||||||
cy.get('app-viewboxes .close', { withinSubject: null }).click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeColumn = (column: string) => {
|
|
||||||
cy.get(`.col-box.column-${column} clr-icon`, { withinSubject: null }).click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const addColumns = (columns: string[], callback?: () => void) => {
|
|
||||||
for (let i = 0; i < columns.length; i++) {
|
|
||||||
const column = columns[i]
|
|
||||||
|
|
||||||
cy.get('.cols-search input', { withinSubject: null }).type(column)
|
|
||||||
cy.get('.cols-search .autocomplete-wrapper', { withinSubject: null })
|
|
||||||
.first()
|
|
||||||
.trigger('keydown', { key: 'ArrowDown' })
|
|
||||||
cy.get('.cols-search .autocomplete-wrapper', { withinSubject: null })
|
|
||||||
.first()
|
|
||||||
.trigger('keydown', { key: 'Enter' })
|
|
||||||
.then(() => {
|
|
||||||
if (i === columns.length - 1 && callback) callback()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openViewboxConfig = (viewbox_tablename: string) => {
|
|
||||||
cy.get('.open-viewbox').then((viewboxes: any) => {
|
|
||||||
for (let openViewbox of viewboxes) {
|
|
||||||
if (openViewbox.innerText.toLowerCase().includes(viewbox_tablename))
|
|
||||||
openViewbox.click()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 viyaLib
|
|
||||||
|
|
||||||
for (let node of treeNodes) {
|
|
||||||
if (new RegExp(libNameIncludes).test(node.innerText.toLowerCase())) {
|
|
||||||
viyaLib = node
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.get(viyaLib).within(() => {
|
|
||||||
cy.get('.clr-tree-node-content-container p').click()
|
|
||||||
|
|
||||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
|
||||||
for (let innerNode of innerNodes) {
|
|
||||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
|
||||||
innerNode.click()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const setFilterWithValue = (
|
|
||||||
variableValue: string,
|
|
||||||
valueString: string,
|
|
||||||
valueField: 'value' | 'time' | 'date' | 'datetime' | 'in' | 'between',
|
|
||||||
callback?: any
|
|
||||||
) => {
|
|
||||||
cy.wait(600)
|
|
||||||
|
|
||||||
cy.focused().type(variableValue)
|
|
||||||
cy.wait(100)
|
|
||||||
// cy.focused().trigger('input')
|
|
||||||
cy.get('.variable-col .autocomplete-wrapper', { withinSubject: null })
|
|
||||||
.first()
|
|
||||||
.trigger('keydown', { key: 'ArrowDown' })
|
|
||||||
cy.get('.variable-col .autocomplete-wrapper', {
|
|
||||||
withinSubject: null
|
|
||||||
}).trigger('keydown', { key: 'Enter' })
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.wait(100)
|
|
||||||
|
|
||||||
if (valueField === 'in') {
|
|
||||||
cy.focused().select(valueField.toUpperCase()).trigger('change')
|
|
||||||
} else if (valueField === 'between') {
|
|
||||||
cy.focused().select(valueField.toUpperCase()).trigger('change')
|
|
||||||
} else {
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.wait(100)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (valueField) {
|
|
||||||
case 'value': {
|
|
||||||
cy.focused().type(valueString)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'time': {
|
|
||||||
cy.focused().type(valueString)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'date': {
|
|
||||||
cy.focused().type(valueString)
|
|
||||||
cy.focused().tab()
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'datetime': {
|
|
||||||
const date = valueString.split(' ')[0]
|
|
||||||
const time = valueString.split(' ')[1]
|
|
||||||
|
|
||||||
cy.focused().type(date)
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.focused().type(time)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'in': {
|
|
||||||
cy.get('.checkbox-vals').then(() => {
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.focused().click()
|
|
||||||
cy.get('.no-values')
|
|
||||||
.should('not.exist')
|
|
||||||
.then(() => {
|
|
||||||
cy.get('clr-checkbox-wrapper input').then((inputs: any) => {
|
|
||||||
inputs[0].click()
|
|
||||||
cy.get('.in-values-modal .modal-footer button').click()
|
|
||||||
|
|
||||||
cy.get('.modal-footer .btn-success-outline').click()
|
|
||||||
|
|
||||||
if (callback) callback()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'between': {
|
|
||||||
cy.focused().tab()
|
|
||||||
|
|
||||||
const start = valueString.split('-')[0]
|
|
||||||
const end = valueString.split('-')[1]
|
|
||||||
|
|
||||||
cy.focused().type(start)
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.focused().type(end)
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.wait(100)
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.wait(100)
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.wait(100)
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.wait(100)
|
|
||||||
cy.focused().tab()
|
|
||||||
cy.wait(100)
|
|
||||||
cy.focused().click()
|
|
||||||
|
|
||||||
if (callback) callback()
|
|
||||||
}
|
|
||||||
|
|
||||||
const openTableFromViewboxTree = (
|
|
||||||
libNameIncludes: string,
|
|
||||||
tablenames: string[]
|
|
||||||
) => {
|
|
||||||
cy.get('.add-new clr-tree > clr-tree-node', {
|
|
||||||
timeout: longerCommandTimeout
|
|
||||||
}).then((treeNodes: any) => {
|
|
||||||
let viyaLib
|
|
||||||
|
|
||||||
for (let node of treeNodes) {
|
|
||||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
|
||||||
viyaLib = node
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.get(viyaLib).within(() => {
|
|
||||||
cy.get('p')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
|
||||||
for (let innerNode of innerNodes) {
|
|
||||||
for (let tablename of tablenames) {
|
|
||||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
|
||||||
innerNode.click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const visitPage = (url: string) => {
|
|
||||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
|
||||||
}
|
|
||||||
BIN
client/libraries/xlsx-0.20.3.tgz
Normal file
BIN
client/libraries/xlsx-0.20.3.tgz
Normal file
Binary file not shown.
@@ -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@15.3.0;handsontable@15.3.0;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@^16.0.1;handsontable@16.2.0;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
|
||||||
},
|
},
|
||||||
(error, json) => {
|
(error, json) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
46
client/lighthouserc.js
Normal file
46
client/lighthouserc.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
module.exports = {
|
||||||
|
ci: {
|
||||||
|
collect: {
|
||||||
|
settings: {
|
||||||
|
preset: 'desktop',
|
||||||
|
chromeFlags: '--no-sandbox --disable-dev-shm-usage'
|
||||||
|
},
|
||||||
|
url: [
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/home/tables',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/editor/DC996664.MPE_X_TEST',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/view/data',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/view/data/DC996664',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/view/data/DC996664.MPE_X_TEST',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups/1',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/view/usernav/users/1',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/home/excel-maps',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/home/excel-maps/BASEL-CR2',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/home/multi-load',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/review/submitted',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/review/approve',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/review/history',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/stage/DC20221006T142649516_059582_7169',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/review/submitted/DC20221006T142649516_059582_7169',
|
||||||
|
'http://localhost:5000/AppStream/clickme/#/system'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
assert: {
|
||||||
|
assertions: {
|
||||||
|
'categories:accessibility': [
|
||||||
|
'error',
|
||||||
|
{ minScore: 1, aggregationMethod: 'median' }
|
||||||
|
],
|
||||||
|
'categories:performance': [
|
||||||
|
'error',
|
||||||
|
{ minScore: 0.4, aggregationMethod: 'median' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
target: 'filesystem',
|
||||||
|
outputDir: './lighthouse-reports'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13830
client/package-lock.json
generated
13830
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -31,26 +31,28 @@
|
|||||||
"sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh",
|
"sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh",
|
||||||
"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",
|
||||||
|
"ng": "ng"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^17.3.3",
|
"@angular/animations": "^19.2.18",
|
||||||
"@angular/cdk": "^17.3.3",
|
"@angular/cdk": "^19.2.19",
|
||||||
"@angular/common": "^17.3.3",
|
"@angular/common": "^19.2.18",
|
||||||
"@angular/compiler": "^17.3.3",
|
"@angular/compiler": "^19.2.18",
|
||||||
"@angular/core": "^17.3.3",
|
"@angular/core": "^19.2.18",
|
||||||
"@angular/forms": "^17.3.3",
|
"@angular/forms": "^19.2.18",
|
||||||
"@angular/platform-browser": "^17.3.3",
|
"@angular/platform-browser": "^19.2.18",
|
||||||
"@angular/platform-browser-dynamic": "^17.3.3",
|
"@angular/platform-browser-dynamic": "^19.2.18",
|
||||||
"@angular/router": "^17.3.3",
|
"@angular/router": "^19.2.18",
|
||||||
"@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": "^15.3.0",
|
"@handsontable/angular-wrapper": "16.0.1",
|
||||||
"@sasjs/adapter": "^4.12.1",
|
"@sasjs/adapter": "^4.16.3",
|
||||||
"@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",
|
||||||
@@ -60,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": "^15.3.0",
|
"handsontable": "^16.0.1",
|
||||||
"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": "^10.2.0",
|
"jsrsasign": "^11.1.0",
|
||||||
"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",
|
||||||
@@ -80,21 +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.19",
|
||||||
"@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.19",
|
||||||
"@angular/compiler-cli": "^17.3.3",
|
"@angular/compiler-cli": "^19.2.18",
|
||||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||||
"@compodoc/compodoc": "^1.1.21",
|
"@compodoc/compodoc": "^1.1.21",
|
||||||
"@cypress/webpack-preprocessor": "^5.17.1",
|
"@cypress/webpack-preprocessor": "^5.17.1",
|
||||||
|
"@lhci/cli": "^0.12.0",
|
||||||
"@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",
|
||||||
@@ -126,7 +129,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -167,7 +168,7 @@ export class AutomaticComponent implements OnInit {
|
|||||||
public async getAdminGroups() {
|
public async getAdminGroups() {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
this.adminGroupsLoading = true
|
this.adminGroupsLoading = true
|
||||||
this.sasViyaService
|
;(this.sasViyaService
|
||||||
.getAdminGroups()
|
.getAdminGroups()
|
||||||
.subscribe((res: ViyaApiIdentities) => {
|
.subscribe((res: ViyaApiIdentities) => {
|
||||||
this.adminGroupsLoading = false
|
this.adminGroupsLoading = false
|
||||||
@@ -187,7 +188,7 @@ export class AutomaticComponent implements OnInit {
|
|||||||
this.eventService.showAbortModal('admin groups', err)
|
this.eventService.showAbortModal('admin groups', err)
|
||||||
|
|
||||||
reject(err)
|
reject(err)
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -408,12 +408,11 @@
|
|||||||
<div class="hot-wrapper clr-flex-1">
|
<div class="hot-wrapper clr-flex-1">
|
||||||
<hot-table
|
<hot-table
|
||||||
#hotInstance
|
#hotInstance
|
||||||
hotId="hotInstance"
|
|
||||||
id="hotTable"
|
id="hotTable"
|
||||||
class="edit-hot"
|
class="edit-hot"
|
||||||
className="htDark"
|
|
||||||
[class.hidden]="hotTable.hidden"
|
[class.hidden]="hotTable.hidden"
|
||||||
[licenseKey]="hotTable.licenseKey"
|
[data]="hotTable.data"
|
||||||
|
[settings]="hotTableSettings"
|
||||||
>
|
>
|
||||||
</hot-table>
|
</hot-table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
QueryList,
|
QueryList,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
@@ -16,7 +17,7 @@ import { SasStoreService } from '../services/sas-store.service'
|
|||||||
|
|
||||||
type AOA = any[][]
|
type AOA = any[][]
|
||||||
|
|
||||||
import { HotTableRegisterer } from '@handsontable/angular'
|
import { HotTableComponent } from '@handsontable/angular-wrapper'
|
||||||
import { UploadFile } from '@sasjs/adapter'
|
import { UploadFile } from '@sasjs/adapter'
|
||||||
import { isSpecialMissing } from '@sasjs/utils/input/validators'
|
import { isSpecialMissing } from '@sasjs/utils/input/validators'
|
||||||
import CellRange from 'handsontable/3rdparty/walkontable/src/cell/range'
|
import CellRange from 'handsontable/3rdparty/walkontable/src/cell/range'
|
||||||
@@ -42,6 +43,7 @@ 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 { 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'
|
||||||
@@ -69,15 +71,16 @@ 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 {
|
export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
@ViewChildren('uploadStater')
|
@ViewChildren('uploadStater')
|
||||||
uploadStaterCompList: QueryList<UploadStaterComponent> = new QueryList()
|
uploadStaterCompList: QueryList<UploadStaterComponent> = new QueryList()
|
||||||
@ViewChildren('queryFilter')
|
@ViewChildren('queryFilter')
|
||||||
queryFilterCompList: QueryList<QueryComponent> = new QueryList()
|
queryFilterCompList: QueryList<QueryComponent> = new QueryList()
|
||||||
@ViewChildren('hotInstance')
|
@ViewChild(HotTableComponent, { static: false })
|
||||||
hotInstanceCompList: QueryList<Handsontable> = new QueryList()
|
hotTableComponent!: HotTableComponent
|
||||||
@ViewChildren('fileUploadInput')
|
@ViewChildren('fileUploadInput')
|
||||||
fileUploadInputCompList: QueryList<ElementRef> = new QueryList()
|
fileUploadInputCompList: QueryList<ElementRef> = new QueryList()
|
||||||
|
|
||||||
@@ -119,13 +122,26 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
public hotInstance!: Handsontable
|
public hotInstance!: Handsontable
|
||||||
public dcValidator: DcValidator | undefined
|
public dcValidator: DcValidator | undefined
|
||||||
|
|
||||||
|
public hotTableSettings: Handsontable.GridSettings = {}
|
||||||
|
|
||||||
|
private updateHotTableSettings(): void {
|
||||||
|
this.hotTableSettings = {
|
||||||
|
colHeaders: this.hotTable.colHeaders,
|
||||||
|
columns: this.hotTable.columns,
|
||||||
|
height: this.hotTable.height,
|
||||||
|
licenseKey: this.hotTable.licenseKey,
|
||||||
|
readOnly: this.hotTable.readOnly,
|
||||||
|
copyPaste: this.hotTable.copyPaste,
|
||||||
|
contextMenu: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public hotTable: HotTableInterface = {
|
public hotTable: HotTableInterface = {
|
||||||
data: [],
|
data: [],
|
||||||
colHeaders: [],
|
colHeaders: [],
|
||||||
hidden: true,
|
hidden: true,
|
||||||
columns: [],
|
columns: [],
|
||||||
height: '100%',
|
height: 'calc(100vh - 160px)',
|
||||||
minSpareRows: 1,
|
|
||||||
licenseKey: undefined,
|
licenseKey: undefined,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
copyPaste: {
|
copyPaste: {
|
||||||
@@ -162,10 +178,30 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
row_above: {
|
row_above: {
|
||||||
name: 'Insert Row above'
|
name: 'Insert Row above',
|
||||||
|
callback: (
|
||||||
|
key: string,
|
||||||
|
selection: any[],
|
||||||
|
clickEvent: MouseEvent
|
||||||
|
) => {
|
||||||
|
const firstSelection = selection[0]
|
||||||
|
const targetRow = firstSelection.start.row
|
||||||
|
|
||||||
|
this.insertRowAtPosition(targetRow)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
row_below: {
|
row_below: {
|
||||||
name: 'Insert Row below'
|
name: 'Insert Row below',
|
||||||
|
callback: (
|
||||||
|
key: string,
|
||||||
|
selection: any[],
|
||||||
|
clickEvent: MouseEvent
|
||||||
|
) => {
|
||||||
|
const firstSelection = selection[0]
|
||||||
|
const targetRow = firstSelection.start.row + 1
|
||||||
|
|
||||||
|
this.insertRowAtPosition(targetRow)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
remove_row: {
|
remove_row: {
|
||||||
name: 'Ignore row'
|
name: 'Ignore row'
|
||||||
@@ -350,6 +386,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
public licenceState = this.licenceService.licenceState
|
public licenceState = this.licenceService.licenceState
|
||||||
|
|
||||||
|
private ariaObserver: MutationObserver | undefined
|
||||||
|
private ariaCheckInterval: any | undefined
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private licenceService: LicenceService,
|
private licenceService: LicenceService,
|
||||||
private eventService: EventService,
|
private eventService: EventService,
|
||||||
@@ -360,15 +399,12 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private sasService: SasService,
|
private sasService: SasService,
|
||||||
private cdf: ChangeDetectorRef,
|
private cdf: ChangeDetectorRef,
|
||||||
private hotRegisterer: HotTableRegisterer,
|
|
||||||
private spreadsheetService: SpreadsheetService
|
private spreadsheetService: SpreadsheetService
|
||||||
) {
|
) {
|
||||||
const lang = languages[window.navigator.language]
|
const lang = languages[window.navigator.language]
|
||||||
if (lang)
|
if (lang)
|
||||||
numbro.default.registerLanguage(languages[window.navigator.language])
|
numbro.default.registerLanguage(languages[window.navigator.language])
|
||||||
|
|
||||||
this.hotRegisterer = new HotTableRegisterer()
|
|
||||||
|
|
||||||
this.parseRestrictions()
|
this.parseRestrictions()
|
||||||
this.setRestrictions()
|
this.setRestrictions()
|
||||||
}
|
}
|
||||||
@@ -896,6 +932,11 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.reSetCellValidationValues()
|
this.reSetCellValidationValues()
|
||||||
|
|
||||||
|
// Fix ARIA accessibility issues after table edit
|
||||||
|
setTimeout(() => {
|
||||||
|
this.fixAriaAccessibility()
|
||||||
|
}, 100)
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -922,6 +963,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
this.cellValidationSource = []
|
this.cellValidationSource = []
|
||||||
|
|
||||||
|
// Clear custom validation styling
|
||||||
|
this.clearDuplicateValidation()
|
||||||
|
|
||||||
const hot = this.hotInstance
|
const hot = this.hotInstance
|
||||||
const columnSorting = hot.getPlugin('multiColumnSorting')
|
const columnSorting = hot.getPlugin('multiColumnSorting')
|
||||||
const columnSortConfig = columnSorting.getSortConfig()
|
const columnSortConfig = columnSorting.getSortConfig()
|
||||||
@@ -982,22 +1026,58 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const hot = this.hotInstance
|
const hot = this.hotInstance
|
||||||
|
|
||||||
const dsInsertIndex = this.dataSource.length
|
// Create a new empty row object with proper structure
|
||||||
hot.alter('insert_row_below', dsInsertIndex, 1)
|
const newRow = this.createEmptyRow()
|
||||||
|
|
||||||
|
// Add the new row to the data source
|
||||||
|
this.dataSource.push(newRow)
|
||||||
|
|
||||||
|
// Update the hot table with the new data
|
||||||
hot.updateSettings({ data: this.dataSource }, false)
|
hot.updateSettings({ data: this.dataSource }, false)
|
||||||
|
|
||||||
|
// Select the newly added row
|
||||||
hot.selectCell(this.dataSource.length - 1, 0)
|
hot.selectCell(this.dataSource.length - 1, 0)
|
||||||
hot.render()
|
hot.render()
|
||||||
|
|
||||||
if (this.dataSource[dsInsertIndex]) {
|
|
||||||
this.dataSource[dsInsertIndex]['noLinkOption'] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addingNewRow = false
|
this.addingNewRow = false
|
||||||
|
|
||||||
this.reSetCellValidationValues()
|
this.reSetCellValidationValues()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new empty row object with proper structure.
|
||||||
|
* Columns with NOTNULL DQ rules are pre-populated with their RULE_VALUE.
|
||||||
|
*/
|
||||||
|
private createEmptyRow(): any {
|
||||||
|
const newRow: any = {}
|
||||||
|
this.cellValidation.forEach((rule: any) => {
|
||||||
|
const dataKey = rule.data
|
||||||
|
newRow[dataKey] = this.hotDataSchema.hasOwnProperty(dataKey)
|
||||||
|
? this.hotDataSchema[dataKey]
|
||||||
|
: ''
|
||||||
|
})
|
||||||
|
newRow['noLinkOption'] = true
|
||||||
|
return newRow
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a new row at the specified position and updates the table
|
||||||
|
*/
|
||||||
|
private insertRowAtPosition(targetRow: number): void {
|
||||||
|
const newRow = this.createEmptyRow()
|
||||||
|
|
||||||
|
// Insert the new row at the target position
|
||||||
|
this.dataSource.splice(targetRow, 0, newRow)
|
||||||
|
|
||||||
|
// Update the hot table
|
||||||
|
const hot = this.hotInstance
|
||||||
|
hot.updateSettings({ data: this.dataSource }, false)
|
||||||
|
hot.render()
|
||||||
|
|
||||||
|
this.reSetCellValidationValues()
|
||||||
|
}
|
||||||
|
|
||||||
public cancelSubmit() {
|
public cancelSubmit() {
|
||||||
this.dataSource = this.helperService.deepClone(this.dataSourceBeforeSubmit)
|
this.dataSource = this.helperService.deepClone(this.dataSourceBeforeSubmit)
|
||||||
this.dataSourceBeforeSubmit = []
|
this.dataSourceBeforeSubmit = []
|
||||||
@@ -1077,51 +1157,96 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public validatePrimaryKeys() {
|
private clearDuplicateValidation() {
|
||||||
const hot = this.hotInstance
|
const hot = this.hotInstance
|
||||||
|
|
||||||
const myTable = hot.getData()
|
// Clear previous duplicate validation styling
|
||||||
this.pkFields = []
|
for (const rowIndex of this.duplicatePkIndexes) {
|
||||||
for (let index = 0; index < myTable.length; index++) {
|
for (let col = 1; col <= this.readOnlyFields; col++) {
|
||||||
let pkRow = ''
|
hot.removeCellMeta(rowIndex, col, 'valid')
|
||||||
for (let ind = 1; ind < this.readOnlyFields + 1; ind++) {
|
hot.removeCellMeta(rowIndex, col, 'dupKey')
|
||||||
pkRow = pkRow + '|' + myTable[index][ind]
|
// Remove our custom class from cell metadata
|
||||||
}
|
const cellMeta = hot.getCellMeta(rowIndex, col)
|
||||||
this.pkFields.push(pkRow)
|
if (cellMeta.className) {
|
||||||
}
|
let cleanedClassName: string
|
||||||
|
if (Array.isArray(cellMeta.className)) {
|
||||||
const results = []
|
cleanedClassName = cellMeta.className
|
||||||
const rows = this.dataSource.length
|
.filter((c) => c !== 'dc-invalid-cell')
|
||||||
|
.join(' ')
|
||||||
for (let j = 0; j < this.pkFields.length; j++) {
|
} else {
|
||||||
for (let i = 0; i < this.pkFields.length; i++) {
|
cleanedClassName = cellMeta.className
|
||||||
if (this.pkFields[j] === this.pkFields[i] && i !== j) {
|
.replace('dc-invalid-cell', '')
|
||||||
results.push(i)
|
.trim()
|
||||||
|
}
|
||||||
|
hot.setCellMeta(rowIndex, col, 'className', cleanedClassName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.pkFields.length > rows) {
|
this.duplicatePkIndexes = []
|
||||||
for (let n = rows; n < this.pkFields.length; n++) {
|
hot.render()
|
||||||
for (let p = rows; p < this.pkFields.length; p++) {
|
}
|
||||||
if (n < p && this.pkFields[n] === this.pkFields[p]) {
|
|
||||||
results.push(p)
|
public validatePrimaryKeys() {
|
||||||
|
const hot = this.hotInstance
|
||||||
|
|
||||||
|
// Clear previous validation before applying new ones
|
||||||
|
this.clearDuplicateValidation()
|
||||||
|
|
||||||
|
// Get data from the data source instead of hot.getData() to ensure consistency
|
||||||
|
const myTable = this.dataSource
|
||||||
|
this.pkFields = []
|
||||||
|
|
||||||
|
for (let index = 0; index < myTable.length; index++) {
|
||||||
|
let pkRow = ''
|
||||||
|
for (let ind = 1; ind < this.readOnlyFields + 1; ind++) {
|
||||||
|
const colName = this.headerColumns[ind]
|
||||||
|
const value = myTable[index][colName] || ''
|
||||||
|
pkRow = pkRow + '|' + value
|
||||||
|
}
|
||||||
|
this.pkFields.push(pkRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: any = []
|
||||||
|
const rows = this.dataSource.length
|
||||||
|
|
||||||
|
// Only check for duplicates if we have data
|
||||||
|
if (this.pkFields.length > 0) {
|
||||||
|
for (let j = 0; j < this.pkFields.length; j++) {
|
||||||
|
for (let i = 0; i < this.pkFields.length; i++) {
|
||||||
|
if (
|
||||||
|
this.pkFields[j] === this.pkFields[i] &&
|
||||||
|
i !== j &&
|
||||||
|
this.pkFields[j] !== '|'
|
||||||
|
) {
|
||||||
|
results.push(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cellMeta
|
// Clear any existing validation marks for all cells
|
||||||
|
for (let row = 0; row < myTable.length; row++) {
|
||||||
|
for (let col = 0; col < this.headerColumns.length; col++) {
|
||||||
|
const cellMeta = hot.getCellMeta(row, col)
|
||||||
|
if (cellMeta) {
|
||||||
|
cellMeta.valid = true
|
||||||
|
cellMeta.dupKey = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark duplicate cells as invalid
|
||||||
for (let k = 0; k < results.length; k++) {
|
for (let k = 0; k < results.length; k++) {
|
||||||
for (let index = 1; index < this.readOnlyFields + 1; index++) {
|
for (let index = 1; index < this.readOnlyFields + 1; index++) {
|
||||||
cellMeta = hot.getCellMeta(results[k], index)
|
hot.setCellMeta(results[k], index, 'valid', false)
|
||||||
cellMeta.valid = false
|
hot.setCellMeta(results[k], index, 'dupKey', true)
|
||||||
cellMeta.dupKey = true
|
hot.setCellMeta(results[k], index, 'className', 'dc-invalid-cell')
|
||||||
hot.render()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.duplicatePkIndexes = [...new Set(results.sort())]
|
this.duplicatePkIndexes = [...new Set(results.sort())]
|
||||||
|
hot.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1416,10 +1541,26 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
this.dataSourceBeforeSubmit = this.helperService.deepClone(this.dataSource)
|
this.dataSourceBeforeSubmit = this.helperService.deepClone(this.dataSource)
|
||||||
|
|
||||||
|
// Clean up the data source by removing noLinkOption property
|
||||||
for (let i = 0; i < this.dataSource.length; i++) {
|
for (let i = 0; i < this.dataSource.length; i++) {
|
||||||
delete this.dataSource[i].noLinkOption
|
delete this.dataSource[i].noLinkOption
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove any completely empty rows from the end
|
||||||
|
while (this.dataSource.length > 0) {
|
||||||
|
const lastRow = this.dataSource[this.dataSource.length - 1]
|
||||||
|
const isEmpty = Object.keys(lastRow).every((key) => {
|
||||||
|
if (key === '_____DELETE__THIS__RECORD_____') return true
|
||||||
|
return !lastRow[key] || lastRow[key] === ''
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
this.dataSource.pop()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hot.updateSettings(
|
hot.updateSettings(
|
||||||
{
|
{
|
||||||
data: this.dataSource,
|
data: this.dataSource,
|
||||||
@@ -1437,17 +1578,6 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
EditorComponent.cnt = 0
|
EditorComponent.cnt = 0
|
||||||
EditorComponent.nonPkCnt = 0
|
EditorComponent.nonPkCnt = 0
|
||||||
// this.saveLoading = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Below code should be analized, not sure what is the purpose of exceedCells
|
|
||||||
*/
|
|
||||||
const myTableData = hot.getData()
|
|
||||||
|
|
||||||
// If the last row is empty, remove it before validation
|
|
||||||
if (myTableData.length > 1 && hot.isEmptyRow(myTableData.length - 1)) {
|
|
||||||
hot.alter('remove_row', myTableData.length - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.validatePrimaryKeys()
|
this.validatePrimaryKeys()
|
||||||
|
|
||||||
@@ -1477,15 +1607,6 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
if (txt) txt.focus()
|
if (txt) txt.focus()
|
||||||
}, 200)
|
}, 200)
|
||||||
})
|
})
|
||||||
|
|
||||||
// let cnt = 0;
|
|
||||||
// hot.addHook("afterValidate", () => {
|
|
||||||
// this.updateSoftSelectColumns(true);
|
|
||||||
// cnt++;
|
|
||||||
// if (cnt === long) {
|
|
||||||
// this.validationDone = 1;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async saveTable(data: any) {
|
public async saveTable(data: any) {
|
||||||
@@ -1639,11 +1760,20 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public checkInvalid() {
|
public checkInvalid() {
|
||||||
const hotElement = (this.hotInstanceCompList.first.container as any)
|
// Use Angular wrapper to access Handsontable element instead of DOM queries
|
||||||
.nativeElement
|
if (!this.hotTableComponent || !this.hotTableComponent.hotInstance)
|
||||||
const invalidCells = hotElement.querySelectorAll('.htInvalid')
|
return false
|
||||||
|
|
||||||
return invalidCells.length > 0
|
const hotElement = this.hotTableComponent.hotInstance.rootElement
|
||||||
|
if (!hotElement) return false
|
||||||
|
|
||||||
|
// Check for standard Handsontable validation failures
|
||||||
|
const standardInvalidCells = hotElement.querySelectorAll('.htInvalid')
|
||||||
|
|
||||||
|
// Check for our custom duplicate primary key validation failures
|
||||||
|
const customInvalidCells = hotElement.querySelectorAll('.dc-invalid-cell')
|
||||||
|
|
||||||
|
return standardInvalidCells.length > 0 || customInvalidCells.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
public goToEditor() {
|
public goToEditor() {
|
||||||
@@ -2183,6 +2313,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
*/
|
*/
|
||||||
private setCellFilter(filter: boolean) {
|
private setCellFilter(filter: boolean) {
|
||||||
const hotSelected = this.hotInstance.getSelected()
|
const hotSelected = this.hotInstance.getSelected()
|
||||||
|
if (!hotSelected) return
|
||||||
const selection = hotSelected ? hotSelected[0] : hotSelected
|
const selection = hotSelected ? hotSelected[0] : hotSelected
|
||||||
|
|
||||||
// When we open a dropdown we want filter disabled so value in cell
|
// When we open a dropdown we want filter disabled so value in cell
|
||||||
@@ -2207,9 +2338,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
// Initialize hot table settings
|
||||||
|
this.updateHotTableSettings()
|
||||||
|
|
||||||
this.licenceService.hot_license_key.subscribe(
|
this.licenceService.hot_license_key.subscribe(
|
||||||
(hot_license_key: string | undefined) => {
|
(hot_license_key: string | undefined) => {
|
||||||
this.hotTable.licenseKey = hot_license_key
|
this.hotTable.licenseKey = hot_license_key
|
||||||
|
this.updateHotTableSettings() // Update settings when license key changes
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2262,14 +2397,202 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {}
|
ngAfterViewInit() {
|
||||||
|
// Fix ARIA accessibility issues after table initialization
|
||||||
|
setTimeout(() => {
|
||||||
|
this.fixAriaAccessibility()
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// Set up event listener for hot table element
|
||||||
|
// Double click to edit
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.hotTableComponent && this.hotTableComponent.hotInstance) {
|
||||||
|
const hotElement = this.hotTableComponent.hotInstance.rootElement
|
||||||
|
if (hotElement) {
|
||||||
|
hotElement.addEventListener('mousedown', (event: MouseEvent) => {
|
||||||
|
if (!this.uploadPreview) {
|
||||||
|
this.hotClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const menuDebugItem: any =
|
||||||
|
document.querySelector('.debug-switch-item') || undefined
|
||||||
|
if (menuDebugItem) menuDebugItem.click()
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
// Clean up the MutationObserver
|
||||||
|
if (this.ariaObserver) {
|
||||||
|
this.ariaObserver.disconnect()
|
||||||
|
this.ariaObserver = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the interval
|
||||||
|
if (this.ariaCheckInterval) {
|
||||||
|
clearInterval(this.ariaCheckInterval)
|
||||||
|
this.ariaCheckInterval = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixes ARIA accessibility issues in the Handsontable component
|
||||||
|
* This addresses the accessibility report issues with treegrid and presentation roles
|
||||||
|
*/
|
||||||
|
private fixAriaAccessibility() {
|
||||||
|
// Use a more aggressive approach to find and fix all ARIA issues
|
||||||
|
const fixAriaIssues = () => {
|
||||||
|
// Specifically target Handsontable wrapper elements that are causing issues
|
||||||
|
const hotWrappers = document.querySelectorAll(
|
||||||
|
'.ht-wrapper, .wtHolder, [id^="ht_"]'
|
||||||
|
)
|
||||||
|
|
||||||
|
hotWrappers.forEach((wrapper) => {
|
||||||
|
// Remove problematic ARIA attributes from Handsontable wrappers
|
||||||
|
wrapper.removeAttribute('role')
|
||||||
|
wrapper.removeAttribute('aria-rowcount')
|
||||||
|
wrapper.removeAttribute('aria-colcount')
|
||||||
|
wrapper.removeAttribute('aria-multiselectable')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find all elements with problematic ARIA roles in the entire document
|
||||||
|
const allTreegridElements = document.querySelectorAll('[role="treegrid"]')
|
||||||
|
const allPresentationElements = document.querySelectorAll(
|
||||||
|
'[role="presentation"]'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fix treegrid role issues - remove them completely as they're causing problems
|
||||||
|
allTreegridElements.forEach((element) => {
|
||||||
|
element.removeAttribute('role')
|
||||||
|
element.removeAttribute('aria-rowcount')
|
||||||
|
element.removeAttribute('aria-colcount')
|
||||||
|
element.removeAttribute('aria-multiselectable')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fix presentation role issues - remove them if they contain interactive elements
|
||||||
|
allPresentationElements.forEach((element) => {
|
||||||
|
const hasInteractiveChildren =
|
||||||
|
element.querySelectorAll(
|
||||||
|
'button, input, select, textarea, [tabindex], [onclick], [contenteditable]'
|
||||||
|
).length > 0
|
||||||
|
if (hasInteractiveChildren) {
|
||||||
|
element.removeAttribute('role')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Also fix any elements with aria-rowcount="-1" which is problematic
|
||||||
|
const negativeRowCountElements = document.querySelectorAll(
|
||||||
|
'[aria-rowcount="-1"]'
|
||||||
|
)
|
||||||
|
negativeRowCountElements.forEach((element) => {
|
||||||
|
element.removeAttribute('aria-rowcount')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure proper table structure
|
||||||
|
const tableElements = document.querySelectorAll('table')
|
||||||
|
tableElements.forEach((table) => {
|
||||||
|
if (!table.getAttribute('role')) {
|
||||||
|
table.setAttribute('role', 'table')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure table headers have proper scope
|
||||||
|
const headerCells = table.querySelectorAll('th')
|
||||||
|
headerCells.forEach((th) => {
|
||||||
|
if (!th.getAttribute('scope')) {
|
||||||
|
th.setAttribute('scope', 'col')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add proper ARIA labels to interactive elements
|
||||||
|
const interactiveElements = document.querySelectorAll(
|
||||||
|
'button, input, select, textarea, [contenteditable]'
|
||||||
|
)
|
||||||
|
interactiveElements.forEach((element) => {
|
||||||
|
if (
|
||||||
|
!element.getAttribute('aria-label') &&
|
||||||
|
!element.getAttribute('aria-labelledby')
|
||||||
|
) {
|
||||||
|
const textContent = element.textContent?.trim()
|
||||||
|
if (textContent) {
|
||||||
|
element.setAttribute('aria-label', textContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the fix immediately
|
||||||
|
fixAriaIssues()
|
||||||
|
|
||||||
|
// Run it again after a short delay to catch any dynamically created elements
|
||||||
|
setTimeout(fixAriaIssues, 100)
|
||||||
|
setTimeout(fixAriaIssues, 500)
|
||||||
|
setTimeout(fixAriaIssues, 1000)
|
||||||
|
setTimeout(fixAriaIssues, 2000)
|
||||||
|
|
||||||
|
// Set up a periodic check to ensure accessibility fixes are maintained
|
||||||
|
if (!this.ariaCheckInterval) {
|
||||||
|
this.ariaCheckInterval = setInterval(fixAriaIssues, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up a MutationObserver to continuously monitor for new problematic elements
|
||||||
|
if (!this.ariaObserver) {
|
||||||
|
this.ariaObserver = new MutationObserver((mutations) => {
|
||||||
|
let shouldFix = false
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (
|
||||||
|
mutation.type === 'attributes' &&
|
||||||
|
(mutation.attributeName === 'role' ||
|
||||||
|
mutation.attributeName === 'aria-rowcount')
|
||||||
|
) {
|
||||||
|
shouldFix = true
|
||||||
|
}
|
||||||
|
if (mutation.type === 'childList') {
|
||||||
|
mutation.addedNodes.forEach((node) => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const element = node as Element
|
||||||
|
if (
|
||||||
|
element.hasAttribute('role') ||
|
||||||
|
element.hasAttribute('aria-rowcount')
|
||||||
|
) {
|
||||||
|
shouldFix = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (shouldFix) {
|
||||||
|
setTimeout(fixAriaIssues, 50)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start observing the entire document for changes
|
||||||
|
this.ariaObserver.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: [
|
||||||
|
'role',
|
||||||
|
'aria-rowcount',
|
||||||
|
'aria-colcount',
|
||||||
|
'aria-multiselectable'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
initSetup(response: EditorsGetDataServiceResponse) {
|
initSetup(response: EditorsGetDataServiceResponse) {
|
||||||
this.hotInstance = this.hotRegisterer.getInstance('hotInstance')
|
this.hotInstance = this.hotTableComponent!.hotInstance!
|
||||||
|
|
||||||
if (this.getdataError) return
|
if (this.getdataError) return
|
||||||
if (!response) return
|
if (!response) return
|
||||||
if (!response.data) return
|
if (!response.data) return
|
||||||
|
if (!this.hotInstance) return
|
||||||
|
|
||||||
this.cols = response.data.cols
|
this.cols = response.data.cols
|
||||||
this.dsmeta = response.data.dsmeta
|
this.dsmeta = response.data.dsmeta
|
||||||
@@ -2289,7 +2612,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
this.dsNote = ''
|
this.dsNote = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const hot: Handsontable = this.hotInstance
|
const hot = this.hotInstance
|
||||||
|
|
||||||
const approvers: Approver[] = response.data.approvers
|
const approvers: Approver[] = response.data.approvers
|
||||||
|
|
||||||
@@ -2358,13 +2681,14 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
// 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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2410,6 +2734,11 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
rowHeights: 24,
|
rowHeights: 24,
|
||||||
maxRows: this.licenceState.value.editor_rows_allowed || Infinity,
|
maxRows: this.licenceState.value.editor_rows_allowed || Infinity,
|
||||||
invalidCellClassName: 'htInvalid',
|
invalidCellClassName: 'htInvalid',
|
||||||
|
// Prevent automatic row creation
|
||||||
|
autoWrapRow: false,
|
||||||
|
autoWrapCol: false,
|
||||||
|
// Ensure proper data binding
|
||||||
|
bindRowsWithHeaders: false,
|
||||||
dropdownMenu: {
|
dropdownMenu: {
|
||||||
items: {
|
items: {
|
||||||
make_read_only: {
|
make_read_only: {
|
||||||
@@ -2484,7 +2813,51 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
cellProperties: Handsontable.CellProperties
|
cellProperties: Handsontable.CellProperties
|
||||||
) => {
|
) => {
|
||||||
const isReadonlyCol = col && this.isReadonlyCol(col)
|
const isReadonlyCol = col && this.isReadonlyCol(col)
|
||||||
if (isReadonlyCol) cellProperties.className = 'readonlyCell'
|
|
||||||
|
// Check if this cell should be marked as invalid due to duplicate primary key values
|
||||||
|
// Only applies to primary key columns (col 1 through readOnlyFields)
|
||||||
|
const isDuplicateCell =
|
||||||
|
this.duplicatePkIndexes.includes(row) &&
|
||||||
|
col >= 1 &&
|
||||||
|
col <= this.readOnlyFields
|
||||||
|
|
||||||
|
// Handle existing CSS classes - Handsontable can provide className as string or array
|
||||||
|
const existingClasses = cellProperties.className || ''
|
||||||
|
let classes: string[]
|
||||||
|
|
||||||
|
if (Array.isArray(existingClasses)) {
|
||||||
|
// If already an array, create a copy
|
||||||
|
classes = [...existingClasses]
|
||||||
|
} else {
|
||||||
|
// If string, split by spaces and filter out empty strings
|
||||||
|
classes = existingClasses
|
||||||
|
.split(' ')
|
||||||
|
.filter((c: string) => c.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add readonlyCell class for readonly columns to maintain original styling
|
||||||
|
if (isReadonlyCol && !classes.includes('readonlyCell')) {
|
||||||
|
classes.push('readonlyCell')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply custom validation styling for duplicate primary key cells
|
||||||
|
// Note: Uses 'dc-invalid-cell' instead of Handsontable's 'htInvalid' class
|
||||||
|
// because Handsontable's internal validation system was removing 'htInvalid'
|
||||||
|
// causing flickering. Our custom class persists reliably.
|
||||||
|
if (isDuplicateCell) {
|
||||||
|
if (!classes.includes('dc-invalid-cell')) {
|
||||||
|
classes.push('dc-invalid-cell')
|
||||||
|
}
|
||||||
|
// Mark cell as invalid to prevent form submission
|
||||||
|
cellProperties.valid = false
|
||||||
|
// Custom flag to identify this as a duplicate key cell for cleanup
|
||||||
|
cellProperties.dupKey = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the combined CSS classes back to the cell
|
||||||
|
if (classes.length > 0) {
|
||||||
|
cellProperties.className = classes.join(' ')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
@@ -2503,22 +2876,6 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
this.columnHeader[0] = 'Delete?'
|
this.columnHeader[0] = 'Delete?'
|
||||||
this.readOnlyFields = response.data.sasparams[0].PKCNT
|
this.readOnlyFields = response.data.sasparams[0].PKCNT
|
||||||
|
|
||||||
const hotInstaceEl = document.getElementById('hotInstance')
|
|
||||||
|
|
||||||
if (hotInstaceEl) {
|
|
||||||
hotInstaceEl.addEventListener('mousedown', (event) => {
|
|
||||||
if (!this.uploadPreview) {
|
|
||||||
this.hotClicked()
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const menuDebugItem: any =
|
|
||||||
document.querySelector('.debug-switch-item') || undefined
|
|
||||||
if (menuDebugItem) menuDebugItem.click()
|
|
||||||
}, 100)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
hot.addHook(
|
hot.addHook(
|
||||||
'afterSelection',
|
'afterSelection',
|
||||||
(
|
(
|
||||||
@@ -2597,6 +2954,17 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
hot.addHook('afterRender', (isForced: boolean) => {
|
hot.addHook('afterRender', (isForced: boolean) => {
|
||||||
this.eventService.dispatchEvent('resize')
|
this.eventService.dispatchEvent('resize')
|
||||||
|
|
||||||
|
// Fix ARIA accessibility issues after each render
|
||||||
|
this.fixAriaAccessibility()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add a more frequent accessibility fix hook
|
||||||
|
hot.addHook('afterChange', () => {
|
||||||
|
// Fix ARIA accessibility issues after any data change
|
||||||
|
setTimeout(() => {
|
||||||
|
this.fixAriaAccessibility()
|
||||||
|
}, 50)
|
||||||
})
|
})
|
||||||
|
|
||||||
hot.addHook('afterCreateRow', (source: any, change: any) => {
|
hot.addHook('afterCreateRow', (source: any, change: any) => {
|
||||||
@@ -2610,6 +2978,44 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add hook to prevent unwanted row creation
|
||||||
|
hot.addHook(
|
||||||
|
'beforeCreateRow',
|
||||||
|
(index: number, amount: number, source?: any) => {
|
||||||
|
// Only allow row creation through the Add Row button or context menu
|
||||||
|
if (
|
||||||
|
!this.addingNewRow &&
|
||||||
|
source !== 'ContextMenu.insert_row_above' &&
|
||||||
|
source !== 'ContextMenu.insert_row_below'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auto-populate NOTNULL default when validation fails due to empty value
|
||||||
|
hot.addHook(
|
||||||
|
'afterValidate',
|
||||||
|
(isValid: boolean, value: any, row: number, prop: string | number) => {
|
||||||
|
if (isValid || !isEmpty(value)) return
|
||||||
|
|
||||||
|
const colName =
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
hot.addHook('beforePaste', (data: any, cords: any) => {
|
hot.addHook('beforePaste', (data: any, cords: any) => {
|
||||||
const startCol = cords[0].startCol
|
const startCol = cords[0].startCol
|
||||||
|
|
||||||
@@ -2660,5 +3066,10 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hot.render()
|
hot.render()
|
||||||
|
|
||||||
|
// Fix ARIA accessibility issues after table initialization
|
||||||
|
setTimeout(() => {
|
||||||
|
this.fixAriaAccessibility()
|
||||||
|
}, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { ClarityModule } from '@clr/angular'
|
import { ClarityModule } from '@clr/angular'
|
||||||
import { HotTableModule } from '@handsontable/angular'
|
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||||
import { registerAllModules } from 'handsontable/registry'
|
import { registerAllModules } from 'handsontable/registry'
|
||||||
import { AppSharedModule } from '../app-shared.module'
|
import { AppSharedModule } from '../app-shared.module'
|
||||||
import { DirectivesModule } from '../directives/directives.module'
|
import { DirectivesModule } from '../directives/directives.module'
|
||||||
@@ -28,7 +28,7 @@ registerAllModules()
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
EditorRoutingModule,
|
EditorRoutingModule,
|
||||||
ClarityModule,
|
ClarityModule,
|
||||||
HotTableModule.forRoot(),
|
HotTableModule,
|
||||||
AppSharedModule,
|
AppSharedModule,
|
||||||
DirectivesModule,
|
DirectivesModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,178 +1,182 @@
|
|||||||
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 currentLicenceKey = this.licenceService.licenceKey
|
public syssite = this.appService.syssite
|
||||||
public currentActivationKey = this.licenceService.activationKey
|
public currentLicenceKey = this.licenceService.licenceKey
|
||||||
public isAppFreeTier = this.licenceService.isAppFreeTier
|
public currentActivationKey = this.licenceService.activationKey
|
||||||
public userCountLimitation = this.licenceService.userCountLimitation
|
public isAppFreeTier = this.licenceService.isAppFreeTier
|
||||||
|
public userCountLimitation = this.licenceService.userCountLimitation
|
||||||
public licenseKeyData: LicenseKeyData | null = null
|
|
||||||
|
public licenseKeyData: LicenseKeyData | null = null
|
||||||
public inputType: 'file' | 'paste' = 'file'
|
|
||||||
public licenceFileError: string | undefined
|
public inputType: 'file' | 'paste' = 'file'
|
||||||
public licenceFileLoading: boolean = false
|
public licenceFileError: string | undefined
|
||||||
public licencefile: { filename: string } = {
|
public licenceFileLoading: boolean = false
|
||||||
filename: ''
|
public licencefile: { filename: string } = {
|
||||||
}
|
filename: ''
|
||||||
|
}
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute,
|
constructor(
|
||||||
private licenceService: LicenceService,
|
private route: ActivatedRoute,
|
||||||
private sasService: SasService,
|
private router: Router,
|
||||||
private appService: AppService
|
private licenceService: LicenceService,
|
||||||
) {}
|
private sasService: SasService,
|
||||||
|
private appService: AppService
|
||||||
ngOnInit(): void {
|
) {}
|
||||||
this.licenceKeyValue = this.currentLicenceKey || ''
|
|
||||||
this.activationKeyValue = this.currentActivationKey || ''
|
ngOnInit(): void {
|
||||||
|
this.licenceKeyValue = this.currentLicenceKey || ''
|
||||||
this.route.queryParams.subscribe((queryParams: any) => {
|
this.activationKeyValue = this.currentActivationKey || ''
|
||||||
this.keyError = queryParams.error
|
|
||||||
this.missmatchedKey = queryParams.missmatchId
|
this.route.queryParams.subscribe((queryParams: any) => {
|
||||||
|
this.keyError = queryParams.error
|
||||||
if (queryParams.details) {
|
this.missmatchedKey = queryParams.missmatchId
|
||||||
this.errorDetails = atob(queryParams.details)
|
|
||||||
}
|
if (queryParams.details) {
|
||||||
})
|
this.errorDetails = atob(queryParams.details)
|
||||||
|
}
|
||||||
this.route.params.subscribe((params: any) => {
|
})
|
||||||
let actionInUrl = params.action
|
|
||||||
|
this.route.params.subscribe((params: any) => {
|
||||||
if (actionInUrl) {
|
let actionInUrl = params.action
|
||||||
if (Object.values(LicenseActions).includes(actionInUrl)) {
|
|
||||||
this.action = actionInUrl
|
if (actionInUrl) {
|
||||||
}
|
if (Object.values(LicenseActions).includes(actionInUrl)) {
|
||||||
}
|
this.action = actionInUrl
|
||||||
})
|
}
|
||||||
|
}
|
||||||
this.licenseKeyData = this.licenceService.getLicenseKeyData()
|
})
|
||||||
}
|
|
||||||
|
this.licenseKeyData = this.licenceService.getLicenseKeyData()
|
||||||
public trimKeys() {
|
}
|
||||||
this.licenceKeyValue = this.licenceKeyValue.trim()
|
|
||||||
this.activationKeyValue = this.activationKeyValue.trim()
|
public trimKeys() {
|
||||||
}
|
this.licenceKeyValue = this.licenceKeyValue.trim()
|
||||||
|
this.activationKeyValue = this.activationKeyValue.trim()
|
||||||
public copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[]) {
|
}
|
||||||
const syssiteString = syssite.join('\n')
|
|
||||||
|
public copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[]) {
|
||||||
navigator.clipboard.writeText(syssiteString).then(() => {
|
const syssiteString = syssite.join('\n')
|
||||||
copyIconRef.setAttribute('shape', 'check')
|
|
||||||
copyIconRef.setAttribute('class', 'is-success')
|
navigator.clipboard.writeText(syssiteString).then(() => {
|
||||||
copyTooltip.innerText = 'Copied!'
|
copyIconRef.setAttribute('shape', 'check')
|
||||||
|
copyIconRef.setAttribute('class', 'is-success')
|
||||||
setTimeout(() => {
|
copyTooltip.innerText = 'Copied!'
|
||||||
copyIconRef.setAttribute('shape', 'copy')
|
|
||||||
copyIconRef.removeAttribute('class')
|
setTimeout(() => {
|
||||||
copyTooltip.innerText = 'Copy to clipboard'
|
copyIconRef.setAttribute('shape', 'copy')
|
||||||
}, 1000)
|
copyIconRef.removeAttribute('class')
|
||||||
})
|
copyTooltip.innerText = 'Copy to clipboard'
|
||||||
}
|
}, 1000)
|
||||||
|
})
|
||||||
public applyKeys() {
|
}
|
||||||
this.applyingKeys = true
|
|
||||||
|
public applyKeys() {
|
||||||
let table = {
|
this.applyingKeys = true
|
||||||
keyupload: [
|
|
||||||
{
|
let table = {
|
||||||
ACTIVATION_KEY: this.activationKeyValue,
|
keyupload: [
|
||||||
LICENCE_KEY: this.licenceKeyValue
|
{
|
||||||
}
|
ACTIVATION_KEY: this.activationKeyValue,
|
||||||
]
|
LICENCE_KEY: this.licenceKeyValue
|
||||||
}
|
}
|
||||||
|
]
|
||||||
this.sasService
|
}
|
||||||
.request('admin/registerkey', table)
|
|
||||||
.then((res: RequestWrapperResponse) => {
|
this.sasService
|
||||||
if (
|
.request('admin/registerkey', table)
|
||||||
res.adapterResponse.return &&
|
.then((res: RequestWrapperResponse) => {
|
||||||
res.adapterResponse.return[0] &&
|
if (
|
||||||
res.adapterResponse.return[0].MSG === 'SUCCESS'
|
res.adapterResponse.return &&
|
||||||
) {
|
res.adapterResponse.return[0] &&
|
||||||
location.replace(location.href.split('#')[0])
|
res.adapterResponse.return[0].MSG === 'SUCCESS'
|
||||||
}
|
) {
|
||||||
})
|
this.router.navigateByUrl('/').then(() => {
|
||||||
.finally(() => {
|
window.location.reload()
|
||||||
this.applyingKeys = false
|
})
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
.finally(() => {
|
||||||
public onFileCapture(event: any, dropped = false) {
|
this.applyingKeys = false
|
||||||
let file = dropped ? event[0] : event.target.files[0]
|
})
|
||||||
this.licencefile.filename = file.name
|
}
|
||||||
|
|
||||||
if (!file) return
|
public onFileCapture(event: any, dropped = false) {
|
||||||
|
let file = dropped ? event[0] : event.target.files[0]
|
||||||
this.licenceFileLoading = true
|
this.licencefile.filename = file.name
|
||||||
|
|
||||||
const reader = new FileReader()
|
if (!file) return
|
||||||
|
|
||||||
reader.onload = (evt) => {
|
this.licenceFileLoading = true
|
||||||
this.licenceFileError = 'Error reading file.'
|
|
||||||
|
const reader = new FileReader()
|
||||||
if (!evt || !evt.target) return
|
|
||||||
if (evt.target.readyState != 2) return
|
reader.onload = (evt) => {
|
||||||
if (evt.target.error) return
|
this.licenceFileError = 'Error reading file.'
|
||||||
if (!evt.target.result) return
|
|
||||||
|
if (!evt || !evt.target) return
|
||||||
this.licenceFileLoading = false
|
if (evt.target.readyState != 2) return
|
||||||
this.licenceFileError = undefined
|
if (evt.target.error) return
|
||||||
const fileArr = evt.target.result.toString().split('\n')
|
if (!evt.target.result) return
|
||||||
this.activationKeyValue = fileArr[1]
|
|
||||||
this.licenceKeyValue = fileArr[0]
|
this.licenceFileLoading = false
|
||||||
}
|
this.licenceFileError = undefined
|
||||||
|
const fileArr = evt.target.result.toString().split('\n')
|
||||||
reader.readAsText(file)
|
this.activationKeyValue = fileArr[1]
|
||||||
}
|
this.licenceKeyValue = fileArr[0]
|
||||||
|
}
|
||||||
public switchType(type: 'paste' | 'file') {
|
|
||||||
this.inputType = type
|
reader.readAsText(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
get disableApplyButton(): boolean {
|
public switchType(type: 'paste' | 'file') {
|
||||||
if (this.licenceKeyValue.length < 1 || this.activationKeyValue.length < 1)
|
this.inputType = type
|
||||||
return true
|
}
|
||||||
if (
|
|
||||||
this.licenceKeyValue === this.currentLicenceKey &&
|
get disableApplyButton(): boolean {
|
||||||
this.activationKeyValue === this.currentActivationKey
|
if (this.licenceKeyValue.length < 1 || this.activationKeyValue.length < 1)
|
||||||
)
|
return true
|
||||||
return true
|
if (
|
||||||
|
this.licenceKeyValue === this.currentLicenceKey &&
|
||||||
return false
|
this.activationKeyValue === this.currentActivationKey
|
||||||
}
|
)
|
||||||
}
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -99,6 +100,11 @@ export class MetadataComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.pageSize = 5
|
this.pageSize = 5
|
||||||
|
|
||||||
|
// Initialize filters for accessibility
|
||||||
|
this.typeFilter = new TypeFilter()
|
||||||
|
this.nameFilter = new NameFilter()
|
||||||
|
this.valueFilter = new ValueFilter()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
globals.metadata.metaDataList &&
|
globals.metadata.metaDataList &&
|
||||||
globals.metadata.metaRepositories &&
|
globals.metadata.metaRepositories &&
|
||||||
|
|||||||
@@ -166,13 +166,10 @@
|
|||||||
>
|
>
|
||||||
|
|
||||||
<hot-table
|
<hot-table
|
||||||
hotId="hotInstanceUserDataset"
|
#hotInstanceUserDataset
|
||||||
id="hotTableUserDataset"
|
id="hotTableUserDataset"
|
||||||
class="mt-15"
|
class="mt-15"
|
||||||
[afterGetColHeader]="afterGetColHeader"
|
[settings]="hotUserDatasetsSettings"
|
||||||
[settings]="hotUserDatasets"
|
|
||||||
[licenseKey]="hotTableLicenseKey"
|
|
||||||
stretchH="all"
|
|
||||||
>
|
>
|
||||||
</hot-table>
|
</hot-table>
|
||||||
|
|
||||||
@@ -360,17 +357,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hot-table
|
<hot-table
|
||||||
hotId="hotInstance"
|
#hotInstanceMain
|
||||||
id="hotTable"
|
id="hotTable"
|
||||||
class="mt-15"
|
class="mt-15"
|
||||||
[afterGetColHeader]="afterGetColHeader"
|
[settings]="hotMainTableSettings"
|
||||||
[className]="['htDark', 'htCustomHidden']"
|
|
||||||
[licenseKey]="hotTableLicenseKey"
|
|
||||||
[multiColumnSorting]="true"
|
|
||||||
[viewportRowRenderingOffset]="50"
|
|
||||||
[manualColumnResize]="true"
|
|
||||||
[filters]="true"
|
|
||||||
stretchH="all"
|
|
||||||
>
|
>
|
||||||
</hot-table>
|
</hot-table>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
OnInit,
|
OnInit,
|
||||||
|
AfterViewInit,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ViewEncapsulation
|
ViewEncapsulation
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
@@ -22,7 +23,7 @@ import { HotTableInterface } from '../models/HotTable.interface'
|
|||||||
import { Col } from '../shared/dc-validator/models/col.model'
|
import { Col } from '../shared/dc-validator/models/col.model'
|
||||||
import { SpreadsheetService } from '../services/spreadsheet.service'
|
import { SpreadsheetService } from '../services/spreadsheet.service'
|
||||||
import Handsontable from 'handsontable'
|
import Handsontable from 'handsontable'
|
||||||
import { HotTableRegisterer } from '@handsontable/angular'
|
import { HotTableComponent } from '@handsontable/angular-wrapper'
|
||||||
import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model'
|
import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model'
|
||||||
import { CellChange, ChangeSource } from 'handsontable/common'
|
import { CellChange, ChangeSource } from 'handsontable/common'
|
||||||
import { baseAfterGetColHeader } from '../shared/utils/hot.utils'
|
import { baseAfterGetColHeader } from '../shared/utils/hot.utils'
|
||||||
@@ -47,9 +48,10 @@ 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 {
|
export class MultiDatasetComponent implements OnInit, AfterViewInit {
|
||||||
@HostBinding('class.content-container') contentContainerClass = true
|
@HostBinding('class.content-container') contentContainerClass = true
|
||||||
@ViewChild('contentArea', { static: true }) contentAreaRef!: ElementRef
|
@ViewChild('contentArea', { static: true }) contentAreaRef!: ElementRef
|
||||||
|
|
||||||
@@ -89,7 +91,13 @@ export class MultiDatasetComponent implements OnInit {
|
|||||||
|
|
||||||
public hotInstance!: Handsontable
|
public hotInstance!: Handsontable
|
||||||
public hotInstanceUserDataset!: Handsontable
|
public hotInstanceUserDataset!: Handsontable
|
||||||
private hotRegisterer: HotTableRegisterer
|
@ViewChild('hotInstanceMain', { static: false })
|
||||||
|
hotTableMainComponent!: HotTableComponent
|
||||||
|
@ViewChild('hotInstanceUserDataset', { static: false })
|
||||||
|
hotTableUserDatasetComponent!: HotTableComponent
|
||||||
|
|
||||||
|
public hotMainTableSettings: Handsontable.GridSettings = {}
|
||||||
|
public hotUserDatasetsSettings: Handsontable.GridSettings = {}
|
||||||
|
|
||||||
public showSubmitReasonModal: boolean = false
|
public showSubmitReasonModal: boolean = false
|
||||||
public submitReasonMessage: string = ''
|
public submitReasonMessage: string = ''
|
||||||
@@ -136,7 +144,36 @@ export class MultiDatasetComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
manualRowMove: true,
|
manualRowMove: true,
|
||||||
columnSorting: true
|
columnSorting: true,
|
||||||
|
afterGetColHeader: baseAfterGetColHeader,
|
||||||
|
stretchH: 'all'
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeHotSettings() {
|
||||||
|
this.hotMainTableSettings = {
|
||||||
|
className: ['htDark'],
|
||||||
|
licenseKey: this.hotTableLicenseKey,
|
||||||
|
multiColumnSorting: true,
|
||||||
|
viewportRowRenderingOffset: 50,
|
||||||
|
manualColumnResize: true,
|
||||||
|
autoColumnSize: true,
|
||||||
|
filters: true,
|
||||||
|
stretchH: 'all',
|
||||||
|
afterGetColHeader: baseAfterGetColHeader,
|
||||||
|
modifyColWidth: this.maxWidthCheker
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude data from settings for HOT v16 - it will be loaded manually
|
||||||
|
const { data, ...settingsWithoutData } = this.hotUserDatasets
|
||||||
|
this.hotUserDatasetsSettings = {
|
||||||
|
...settingsWithoutData,
|
||||||
|
licenseKey: this.hotTableLicenseKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public maxWidthCheker(width: any, col: any) {
|
||||||
|
if (width > 200) return 200
|
||||||
|
else return width
|
||||||
}
|
}
|
||||||
|
|
||||||
public afterGetColHeader = baseAfterGetColHeader
|
public afterGetColHeader = baseAfterGetColHeader
|
||||||
@@ -149,16 +186,28 @@ export class MultiDatasetComponent implements OnInit {
|
|||||||
private spreadsheetService: SpreadsheetService,
|
private spreadsheetService: SpreadsheetService,
|
||||||
private sasService: SasService,
|
private sasService: SasService,
|
||||||
private cdr: ChangeDetectorRef
|
private cdr: ChangeDetectorRef
|
||||||
) {
|
) {}
|
||||||
this.hotRegisterer = new HotTableRegisterer()
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.licenceService.hot_license_key.subscribe(
|
this.licenceService.hot_license_key.subscribe(
|
||||||
(hot_license_key: string | undefined) => {
|
(hot_license_key: string | undefined) => {
|
||||||
this.hotTableLicenseKey = hot_license_key
|
this.hotTableLicenseKey = hot_license_key
|
||||||
|
this.initializeHotSettings()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
this.initializeHotSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
// Ensure HOT instances are properly initialized after view is ready
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.hotTableUserDatasetComponent && !this.hotInstanceUserDataset) {
|
||||||
|
this.initUserInputHot()
|
||||||
|
}
|
||||||
|
if (this.hotTableMainComponent && !this.hotInstance) {
|
||||||
|
this.initHot()
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterContentInit(): void {
|
ngAfterContentInit(): void {
|
||||||
@@ -233,7 +282,10 @@ export class MultiDatasetComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.initUserInputHot()
|
this.initUserInputHot()
|
||||||
this.onAutoDetectColumns()
|
// Call onAutoDetectColumns after HOT is initialized
|
||||||
|
setTimeout(() => {
|
||||||
|
this.onAutoDetectColumns()
|
||||||
|
}, 100)
|
||||||
} else if (matchedExtension === 'csv') {
|
} else if (matchedExtension === 'csv') {
|
||||||
this.onMultiCsvFiles(event.target.files)
|
this.onMultiCsvFiles(event.target.files)
|
||||||
} else {
|
} else {
|
||||||
@@ -392,84 +444,112 @@ export class MultiDatasetComponent implements OnInit {
|
|||||||
|
|
||||||
initHot() {
|
initHot() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.hotInstance = this.hotRegisterer.getInstance('hotInstance')
|
if (this.hotTableMainComponent?.hotInstance) {
|
||||||
|
this.hotInstance = this.hotTableMainComponent.hotInstance
|
||||||
|
|
||||||
// Set height of parsed data to full height of the page content area
|
// Set height of parsed data to full height of the page content area
|
||||||
const contentAreaHeight = this.contentAreaRef.nativeElement.clientHeight
|
const contentAreaHeight = this.contentAreaRef.nativeElement.clientHeight
|
||||||
const hotHeight = `${contentAreaHeight - 160}px`
|
const hotHeight = `${contentAreaHeight - 160}px`
|
||||||
|
|
||||||
if (this.activeParsedDataset) {
|
if (this.activeParsedDataset) {
|
||||||
this.hotInstance.updateSettings({
|
// Update settings without data - data will be loaded manually
|
||||||
data: this.activeParsedDataset.datasource || [],
|
this.hotInstance.updateSettings({
|
||||||
colHeaders: this.activeParsedDataset.datasetInfo.headerColumns,
|
colHeaders: this.activeParsedDataset.datasetInfo.headerColumns,
|
||||||
columns: this.activeParsedDataset.datasetInfo.dcValidator?.getRules(),
|
columns:
|
||||||
readOnly: true,
|
this.activeParsedDataset.datasetInfo.dcValidator?.getRules(),
|
||||||
height: hotHeight || '300px',
|
readOnly: true,
|
||||||
className: 'htDark'
|
height: hotHeight || '300px',
|
||||||
})
|
className: ['htDark']
|
||||||
|
})
|
||||||
|
|
||||||
|
// Trigger change detection to avoid ExpressionChangedAfterItHasBeenCheckedError
|
||||||
|
this.cdr.detectChanges()
|
||||||
|
|
||||||
|
// Load data manually - this is required for HOT v16 Angular wrapper
|
||||||
|
setTimeout(() => {
|
||||||
|
if (
|
||||||
|
this.activeParsedDataset &&
|
||||||
|
this.activeParsedDataset.datasource
|
||||||
|
) {
|
||||||
|
this.hotInstance.loadData(this.activeParsedDataset.datasource)
|
||||||
|
this.hotInstance.render()
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
initUserInputHot() {
|
initUserInputHot() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.hotInstanceUserDataset = this.hotRegisterer.getInstance(
|
if (this.hotTableUserDatasetComponent?.hotInstance) {
|
||||||
'hotInstanceUserDataset'
|
this.hotInstanceUserDataset =
|
||||||
)
|
this.hotTableUserDatasetComponent.hotInstance
|
||||||
|
|
||||||
this.hotInstanceUserDataset.addHook(
|
// Load initial data manually after instance is ready
|
||||||
'beforeChange',
|
setTimeout(() => {
|
||||||
(changes: (CellChange | null)[], source: ChangeSource) => {
|
if (this.hotUserDatasets.data) {
|
||||||
if (changes) {
|
this.hotInstanceUserDataset.loadData(this.hotUserDatasets.data)
|
||||||
for (let change of changes) {
|
this.hotInstanceUserDataset.render()
|
||||||
if (change && change[3]) {
|
}
|
||||||
change[3] = change[3].toUpperCase()
|
}, 50)
|
||||||
|
|
||||||
|
this.hotInstanceUserDataset.addHook(
|
||||||
|
'beforeChange',
|
||||||
|
(changes: (CellChange | null)[], source: ChangeSource) => {
|
||||||
|
if (changes) {
|
||||||
|
for (let change of changes) {
|
||||||
|
if (change && change[3]) {
|
||||||
|
change[3] = change[3].toUpperCase()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
|
||||||
|
|
||||||
this.hotInstanceUserDataset.addHook(
|
this.hotInstanceUserDataset.addHook(
|
||||||
'afterChange',
|
'afterChange',
|
||||||
async (changes: CellChange[] | null, source: ChangeSource) => {
|
async (changes: CellChange[] | null, source: ChangeSource) => {
|
||||||
if (changes) {
|
if (changes) {
|
||||||
if (source === 'edit') {
|
if (source === 'edit') {
|
||||||
await this.onUserInputDatasetsChange()
|
await this.onUserInputDatasetsChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let change of changes) {
|
||||||
|
const row = change[0] as number
|
||||||
|
|
||||||
|
this.markUnmatchedRows(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dynamicCellValidations()
|
||||||
|
|
||||||
|
this.hotInstanceUserDataset.render()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
for (let change of changes) {
|
this.hotInstanceUserDataset.addHook(
|
||||||
const row = change[0] as number
|
'afterRemoveRow',
|
||||||
|
async (
|
||||||
|
index: number,
|
||||||
|
amount: number,
|
||||||
|
physicalRows: number[],
|
||||||
|
source?: Handsontable.ChangeSource | undefined
|
||||||
|
) => {
|
||||||
|
await this.onUserInputDatasetsChange()
|
||||||
|
|
||||||
|
for (let row of physicalRows) {
|
||||||
this.markUnmatchedRows(row)
|
this.markUnmatchedRows(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dynamicCellValidations()
|
|
||||||
|
|
||||||
this.hotInstanceUserDataset.render()
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
|
}, 100)
|
||||||
this.hotInstanceUserDataset.addHook(
|
|
||||||
'afterRemoveRow',
|
|
||||||
async (
|
|
||||||
index: number,
|
|
||||||
amount: number,
|
|
||||||
physicalRows: number[],
|
|
||||||
source?: Handsontable.ChangeSource | undefined
|
|
||||||
) => {
|
|
||||||
await this.onUserInputDatasetsChange()
|
|
||||||
|
|
||||||
for (let row of physicalRows) {
|
|
||||||
this.markUnmatchedRows(row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamicCellValidations() {
|
dynamicCellValidations() {
|
||||||
|
if (!this.hotInstanceUserDataset) return
|
||||||
|
|
||||||
const hotData = this.hotInstanceUserDataset.getData()
|
const hotData = this.hotInstanceUserDataset.getData()
|
||||||
|
|
||||||
hotData.forEach((row, rowIndex) => {
|
hotData.forEach((row, rowIndex) => {
|
||||||
@@ -483,6 +563,8 @@ export class MultiDatasetComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
markUnmatchedRows(row: number) {
|
markUnmatchedRows(row: number) {
|
||||||
|
if (!this.hotInstanceUserDataset) return
|
||||||
|
|
||||||
const dataAtRow = this.hotInstanceUserDataset.getDataAtRow(row) as number[]
|
const dataAtRow = this.hotInstanceUserDataset.getDataAtRow(row) as number[]
|
||||||
const dataset = `${dataAtRow[0]}.${dataAtRow[1]}`
|
const dataset = `${dataAtRow[0]}.${dataAtRow[1]}`
|
||||||
const cellMetaAtRow = this.hotInstanceUserDataset.getCellMetaAtRow(row)
|
const cellMetaAtRow = this.hotInstanceUserDataset.getCellMetaAtRow(row)
|
||||||
@@ -556,6 +638,20 @@ export class MultiDatasetComponent implements OnInit {
|
|||||||
* convention. {@link isValidDatasetFormat}
|
* convention. {@link isValidDatasetFormat}
|
||||||
*/
|
*/
|
||||||
async onAutoDetectColumns() {
|
async onAutoDetectColumns() {
|
||||||
|
// Wait for hotInstanceUserDataset to be ready
|
||||||
|
if (!this.hotInstanceUserDataset) {
|
||||||
|
let attempts = 0
|
||||||
|
const maxAttempts = 20
|
||||||
|
while (!this.hotInstanceUserDataset && attempts < maxAttempts) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
attempts++
|
||||||
|
}
|
||||||
|
if (!this.hotInstanceUserDataset) {
|
||||||
|
console.warn('hotInstanceUserDataset not ready after waiting')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let passwordError = false
|
let passwordError = false
|
||||||
|
|
||||||
await this.parseExcelSheetNames()
|
await this.parseExcelSheetNames()
|
||||||
@@ -616,7 +712,13 @@ export class MultiDatasetComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hotInstanceUserDataset.updateData(hotReadyData)
|
if (this.hotInstanceUserDataset) {
|
||||||
|
// Load data manually - this is required for HOT v16 Angular wrapper
|
||||||
|
setTimeout(() => {
|
||||||
|
this.hotInstanceUserDataset.loadData(hotReadyData)
|
||||||
|
this.hotInstanceUserDataset.render()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
this.dynamicCellValidations()
|
this.dynamicCellValidations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { ClarityModule } from '@clr/angular'
|
import { ClarityModule } from '@clr/angular'
|
||||||
import { HotTableModule } from '@handsontable/angular'
|
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||||
import { registerAllModules } from 'handsontable/registry'
|
import { registerAllModules } from 'handsontable/registry'
|
||||||
import { AppSharedModule } from '../app-shared.module'
|
import { AppSharedModule } from '../app-shared.module'
|
||||||
import { DirectivesModule } from '../directives/directives.module'
|
import { DirectivesModule } from '../directives/directives.module'
|
||||||
|
|||||||
@@ -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,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 {
|
||||||
|
|||||||
@@ -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,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 {
|
||||||
|
|||||||
@@ -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,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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -30,7 +30,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
|
||||||
|
|||||||
@@ -33,12 +33,34 @@
|
|||||||
<div class="clr-col-md-12" ng-if="loaded">
|
<div class="clr-col-md-12" ng-if="loaded">
|
||||||
<div *ngIf="approveList && remained !== 0">
|
<div *ngIf="approveList && remained !== 0">
|
||||||
<clr-datagrid class="datagrid-compact datagrid-custom-footer">
|
<clr-datagrid class="datagrid-compact datagrid-custom-footer">
|
||||||
<clr-dg-column [clrDgField]="'submitter'">SUBMITTER</clr-dg-column>
|
<clr-dg-column [clrDgField]="'submitter'">
|
||||||
<clr-dg-column [clrDgField]="'baseTable'">BASE TABLE</clr-dg-column>
|
SUBMITTER
|
||||||
<clr-dg-column [clrDgField]="'submitted'">SUBMITTED</clr-dg-column>
|
<clr-dg-string-filter
|
||||||
<clr-dg-column [clrDgField]="'submitReason'"
|
[clrDgStringFilter]="submitterFilter"
|
||||||
>SUBMIT REASON</clr-dg-column
|
aria-label="Filter submitter"
|
||||||
>
|
></clr-dg-string-filter>
|
||||||
|
</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'baseTable'">
|
||||||
|
BASE TABLE
|
||||||
|
<clr-dg-string-filter
|
||||||
|
[clrDgStringFilter]="baseTableFilter"
|
||||||
|
aria-label="Filter base table"
|
||||||
|
></clr-dg-string-filter>
|
||||||
|
</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'submitted'">
|
||||||
|
SUBMITTED
|
||||||
|
<clr-dg-string-filter
|
||||||
|
[clrDgStringFilter]="submittedFilter"
|
||||||
|
aria-label="Filter submitted date"
|
||||||
|
></clr-dg-string-filter>
|
||||||
|
</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'submitReason'">
|
||||||
|
SUBMIT REASON
|
||||||
|
<clr-dg-string-filter
|
||||||
|
[clrDgStringFilter]="submitReasonFilter"
|
||||||
|
aria-label="Filter submit reason"
|
||||||
|
></clr-dg-string-filter>
|
||||||
|
</clr-dg-column>
|
||||||
<clr-dg-column>ACTION</clr-dg-column>
|
<clr-dg-column>ACTION</clr-dg-column>
|
||||||
<clr-dg-column>DOWNLOAD</clr-dg-column>
|
<clr-dg-column>DOWNLOAD</clr-dg-column>
|
||||||
|
|
||||||
@@ -51,15 +73,19 @@
|
|||||||
<clr-dg-cell>{{ approveItem.submitReason }}</clr-dg-cell>
|
<clr-dg-cell>{{ approveItem.submitReason }}</clr-dg-cell>
|
||||||
<clr-dg-cell>
|
<clr-dg-cell>
|
||||||
<div
|
<div
|
||||||
class="clr-row"
|
class="clr-row d-flex justify-content-around"
|
||||||
role="tooltip"
|
role="toolbar"
|
||||||
class="d-flex justify-content-around"
|
aria-label="Table actions"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-green"
|
class="column-center links tooltip tooltip-md tooltip-bottom-left color-green"
|
||||||
(click)="getClicked(i)"
|
(click)="getClicked(i)"
|
||||||
>
|
>
|
||||||
<clr-icon shape="check" size="24"></clr-icon>
|
<clr-icon
|
||||||
|
shape="check"
|
||||||
|
size="24"
|
||||||
|
aria-hidden="true"
|
||||||
|
></clr-icon>
|
||||||
<span class="tooltip-content">Go to review page screen</span>
|
<span class="tooltip-content">Go to review page screen</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -70,10 +96,12 @@
|
|||||||
*ngIf="!approveItem.rejectLoading"
|
*ngIf="!approveItem.rejectLoading"
|
||||||
shape="ban"
|
shape="ban"
|
||||||
size="22"
|
size="22"
|
||||||
|
aria-hidden="true"
|
||||||
></clr-icon>
|
></clr-icon>
|
||||||
<clr-spinner
|
<clr-spinner
|
||||||
*ngIf="approveItem.rejectLoading"
|
*ngIf="approveItem.rejectLoading"
|
||||||
[clrSmall]="true"
|
[clrSmall]="true"
|
||||||
|
aria-hidden="true"
|
||||||
></clr-spinner>
|
></clr-spinner>
|
||||||
<span class="tooltip-content">Reject</span>
|
<span class="tooltip-content">Reject</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -81,7 +109,11 @@
|
|||||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue"
|
class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue"
|
||||||
(click)="getTable(approveItem.tableId)"
|
(click)="getTable(approveItem.tableId)"
|
||||||
>
|
>
|
||||||
<clr-icon shape="code" size="28"></clr-icon>
|
<clr-icon
|
||||||
|
shape="code"
|
||||||
|
size="28"
|
||||||
|
aria-hidden="true"
|
||||||
|
></clr-icon>
|
||||||
<span class="tooltip-content">Go to staged data screen</span>
|
<span class="tooltip-content">Go to staged data screen</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SasStoreService } from '../../services/sas-store.service'
|
|||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { SasService } from '../../services/sas.service'
|
import { SasService } from '../../services/sas.service'
|
||||||
import { EventService } from '../../services/event.service'
|
import { EventService } from '../../services/event.service'
|
||||||
|
import { ClrDatagridStringFilterInterface } from '@clr/angular'
|
||||||
|
|
||||||
interface ApproveData {
|
interface ApproveData {
|
||||||
tableId: string
|
tableId: string
|
||||||
@@ -19,6 +20,30 @@ interface ApproveData {
|
|||||||
rejectLoading?: boolean
|
rejectLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SubmitterFilter implements ClrDatagridStringFilterInterface<ApproveData> {
|
||||||
|
accepts(data: ApproveData, search: string): boolean {
|
||||||
|
return data.submitter.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseTableFilter implements ClrDatagridStringFilterInterface<ApproveData> {
|
||||||
|
accepts(data: ApproveData, search: string): boolean {
|
||||||
|
return data.baseTable.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubmittedFilter implements ClrDatagridStringFilterInterface<ApproveData> {
|
||||||
|
accepts(data: ApproveData, search: string): boolean {
|
||||||
|
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<ApproveData> {
|
||||||
|
accepts(data: ApproveData, search: string): boolean {
|
||||||
|
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-approve',
|
selector: 'app-approve',
|
||||||
templateUrl: './approve.component.html',
|
templateUrl: './approve.component.html',
|
||||||
@@ -26,7 +51,8 @@ interface ApproveData {
|
|||||||
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
|
||||||
@@ -35,6 +61,12 @@ export class ApproveComponent implements OnInit {
|
|||||||
public loaded: boolean = false
|
public loaded: boolean = false
|
||||||
public itemsNum: number = 10
|
public itemsNum: number = 10
|
||||||
|
|
||||||
|
// Filter instances for datagrid accessibility
|
||||||
|
public submitterFilter = new SubmitterFilter()
|
||||||
|
public baseTableFilter = new BaseTableFilter()
|
||||||
|
public submittedFilter = new SubmittedFilter()
|
||||||
|
public submitReasonFilter = new SubmitReasonFilter()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private sasStoreService: SasStoreService,
|
private sasStoreService: SasStoreService,
|
||||||
private eventService: EventService,
|
private eventService: EventService,
|
||||||
|
|||||||
@@ -85,16 +85,48 @@
|
|||||||
class="datagrid-history datagrid-custom-footer"
|
class="datagrid-history datagrid-custom-footer"
|
||||||
*ngIf="loaded"
|
*ngIf="loaded"
|
||||||
>
|
>
|
||||||
<clr-dg-column [clrDgField]="'basetable'">BASE_TABLE</clr-dg-column>
|
<clr-dg-column [clrDgField]="'basetable'">
|
||||||
<clr-dg-column [clrDgField]="'status'">STATUS</clr-dg-column>
|
BASE_TABLE
|
||||||
<clr-dg-column [clrDgField]="'submitter'">SUBMITTER</clr-dg-column>
|
<clr-dg-string-filter
|
||||||
<clr-dg-column [clrDgField]="'submittedReason'"
|
[clrDgStringFilter]="baseTableFilter"
|
||||||
>SUBMIT REASON</clr-dg-column
|
aria-label="Filter base table"
|
||||||
>
|
></clr-dg-string-filter>
|
||||||
<clr-dg-column [clrDgField]="'submitted'">SUBMITTED</clr-dg-column>
|
</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'reviewed'"
|
<clr-dg-column [clrDgField]="'status'">
|
||||||
>APPROVED / REJECTED</clr-dg-column
|
STATUS
|
||||||
>
|
<clr-dg-string-filter
|
||||||
|
[clrDgStringFilter]="statusFilter"
|
||||||
|
aria-label="Filter status"
|
||||||
|
></clr-dg-string-filter>
|
||||||
|
</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'submitter'">
|
||||||
|
SUBMITTER
|
||||||
|
<clr-dg-string-filter
|
||||||
|
[clrDgStringFilter]="submitterFilter"
|
||||||
|
aria-label="Filter submitter"
|
||||||
|
></clr-dg-string-filter>
|
||||||
|
</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'submittedReason'">
|
||||||
|
SUBMIT REASON
|
||||||
|
<clr-dg-string-filter
|
||||||
|
[clrDgStringFilter]="submitReasonFilter"
|
||||||
|
aria-label="Filter submit reason"
|
||||||
|
></clr-dg-string-filter>
|
||||||
|
</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'submitted'">
|
||||||
|
SUBMITTED
|
||||||
|
<clr-dg-string-filter
|
||||||
|
[clrDgStringFilter]="submittedFilter"
|
||||||
|
aria-label="Filter submitted date"
|
||||||
|
></clr-dg-string-filter>
|
||||||
|
</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'reviewed'">
|
||||||
|
APPROVED / REJECTED
|
||||||
|
<clr-dg-string-filter
|
||||||
|
[clrDgStringFilter]="reviewedFilter"
|
||||||
|
aria-label="Filter reviewed date"
|
||||||
|
></clr-dg-string-filter>
|
||||||
|
</clr-dg-column>
|
||||||
<clr-dg-column>DOWNLOAD</clr-dg-column>
|
<clr-dg-column>DOWNLOAD</clr-dg-column>
|
||||||
|
|
||||||
<clr-dg-row
|
<clr-dg-row
|
||||||
|
|||||||
@@ -8,6 +8,53 @@ import {
|
|||||||
EventService,
|
EventService,
|
||||||
SasService
|
SasService
|
||||||
} from 'src/app/services'
|
} from 'src/app/services'
|
||||||
|
import { ClrDatagridStringFilterInterface } from '@clr/angular'
|
||||||
|
|
||||||
|
interface HistoryData {
|
||||||
|
tableId: string
|
||||||
|
basetable: string
|
||||||
|
status: string
|
||||||
|
submitter: string
|
||||||
|
submittedReason: string
|
||||||
|
submitted: string
|
||||||
|
reviewed: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseTableFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||||
|
accepts(data: HistoryData, search: string): boolean {
|
||||||
|
return data.basetable.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatusFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||||
|
accepts(data: HistoryData, search: string): boolean {
|
||||||
|
return data.status.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubmitterFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||||
|
accepts(data: HistoryData, search: string): boolean {
|
||||||
|
return data.submitter.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||||
|
accepts(data: HistoryData, search: string): boolean {
|
||||||
|
return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubmittedFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||||
|
accepts(data: HistoryData, search: string): boolean {
|
||||||
|
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReviewedFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||||
|
accepts(data: HistoryData, search: string): boolean {
|
||||||
|
return data.reviewed.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-history',
|
selector: 'app-history',
|
||||||
@@ -16,7 +63,8 @@ import {
|
|||||||
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> = []
|
||||||
@@ -29,6 +77,14 @@ export class HistoryComponent implements OnInit {
|
|||||||
public approveData: any = {}
|
public approveData: any = {}
|
||||||
public sasjsConfig: SASjsConfig = new SASjsConfig()
|
public sasjsConfig: SASjsConfig = new SASjsConfig()
|
||||||
|
|
||||||
|
// Filter instances for datagrid accessibility
|
||||||
|
public baseTableFilter = new BaseTableFilter()
|
||||||
|
public statusFilter = new StatusFilter()
|
||||||
|
public submitterFilter = new SubmitterFilter()
|
||||||
|
public submitReasonFilter = new SubmitReasonFilter()
|
||||||
|
public submittedFilter = new SubmittedFilter()
|
||||||
|
public reviewedFilter = new ReviewedFilter()
|
||||||
|
|
||||||
public histParams: { HIST: number; STARTROW: number; NOBS: number } = {
|
public histParams: { HIST: number; STARTROW: number; NOBS: number } = {
|
||||||
HIST: 0,
|
HIST: 0,
|
||||||
STARTROW: 1,
|
STARTROW: 1,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { ClarityModule } from '@clr/angular'
|
import { ClarityModule } from '@clr/angular'
|
||||||
import { HotTableModule } from '@handsontable/angular'
|
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||||
import { DirectivesModule } from '../directives/directives.module'
|
import { DirectivesModule } from '../directives/directives.module'
|
||||||
import { SharedModule } from '../shared/shared.module'
|
import { SharedModule } from '../shared/shared.module'
|
||||||
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
|
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
|
||||||
@@ -23,7 +23,7 @@ import { HistoryComponent } from './history/history.component'
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
ReviewRoutingModule,
|
ReviewRoutingModule,
|
||||||
ClarityModule,
|
ClarityModule,
|
||||||
HotTableModule.forRoot(),
|
HotTableModule,
|
||||||
DirectivesModule,
|
DirectivesModule,
|
||||||
SharedModule
|
SharedModule
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -44,10 +44,20 @@
|
|||||||
<div *ngIf="submitterList && remained !== 0">
|
<div *ngIf="submitterList && remained !== 0">
|
||||||
<clr-datagrid class="datagrid-compact datagrid-custom-footer">
|
<clr-datagrid class="datagrid-compact datagrid-custom-footer">
|
||||||
<clr-dg-column>BASE TABLE</clr-dg-column>
|
<clr-dg-column>BASE TABLE</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'submitted'">SUBMITTED</clr-dg-column>
|
<clr-dg-column [clrDgField]="'submitted'">
|
||||||
<clr-dg-column [clrDgField]="'submitReason'"
|
SUBMITTED
|
||||||
>SUBMIT REASON</clr-dg-column
|
<clr-dg-string-filter
|
||||||
>
|
[clrDgStringFilter]="submittedFilter"
|
||||||
|
aria-label="Filter submitted date"
|
||||||
|
></clr-dg-string-filter>
|
||||||
|
</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'submitReason'">
|
||||||
|
SUBMIT REASON
|
||||||
|
<clr-dg-string-filter
|
||||||
|
[clrDgStringFilter]="submitReasonFilter"
|
||||||
|
aria-label="Filter submit reason"
|
||||||
|
></clr-dg-string-filter>
|
||||||
|
</clr-dg-column>
|
||||||
<clr-dg-column class="d-flex justify-content-center"
|
<clr-dg-column class="d-flex justify-content-center"
|
||||||
>ACTION</clr-dg-column
|
>ACTION</clr-dg-column
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { SasStoreService, EventService, SasService } from '../../services'
|
import { SasStoreService, EventService, SasService } from '../../services'
|
||||||
|
import { ClrDatagridStringFilterInterface } from '@clr/angular'
|
||||||
|
|
||||||
interface SubmitterData {
|
interface SubmitterData {
|
||||||
tableId: string
|
tableId: string
|
||||||
@@ -16,6 +17,18 @@ interface SubmitterData {
|
|||||||
approver: string
|
approver: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SubmittedFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
|
||||||
|
accepts(data: SubmitterData, search: string): boolean {
|
||||||
|
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
|
||||||
|
accepts(data: SubmitterData, search: string): boolean {
|
||||||
|
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-submitter',
|
selector: 'app-submitter',
|
||||||
templateUrl: './submitter.component.html',
|
templateUrl: './submitter.component.html',
|
||||||
@@ -23,7 +36,8 @@ interface SubmitterData {
|
|||||||
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
|
||||||
@@ -37,6 +51,10 @@ export class SubmitterComponent implements OnInit, AfterViewInit {
|
|||||||
private _readySub!: Subscription
|
private _readySub!: Subscription
|
||||||
private _backToSub!: Subscription
|
private _backToSub!: Subscription
|
||||||
|
|
||||||
|
// Filter instances for datagrid accessibility
|
||||||
|
public submittedFilter = new SubmittedFilter()
|
||||||
|
public submitReasonFilter = new SubmitReasonFilter()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private sasStoreService: SasStoreService,
|
private sasStoreService: SasStoreService,
|
||||||
private eventService: EventService,
|
private eventService: EventService,
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -365,13 +365,18 @@ export class SasService {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err: any) => {
|
(err: any) => {
|
||||||
if (err.error.includes('Unauthorized')) {
|
const errorMessage =
|
||||||
|
typeof err.error === 'string'
|
||||||
|
? err.error
|
||||||
|
: JSON.stringify(err.error || err)
|
||||||
|
|
||||||
|
if (errorMessage.includes('Unauthorized')) {
|
||||||
this.shouldLogin.next(true)
|
this.shouldLogin.next(true)
|
||||||
|
|
||||||
this.shouldLogin.subscribe((res: boolean) => {
|
this.shouldLogin.subscribe((res: boolean) => {
|
||||||
if (res === false) location.reload()
|
if (res === false) location.reload()
|
||||||
})
|
})
|
||||||
} else if (err.error.includes(`Folder doesn't exist.`)) {
|
} else if (errorMessage.includes(`Folder doesn't exist.`)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'SASjs SAS services are not present on the current appLoc.'
|
'SASjs SAS services are not present on the current appLoc.'
|
||||||
)
|
)
|
||||||
@@ -419,7 +424,11 @@ export class SasService {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err: any) => {
|
(err: any) => {
|
||||||
if (err.error.includes(`Folder doesn't exist.`)) {
|
const errorMessage =
|
||||||
|
typeof err.error === 'string'
|
||||||
|
? err.error
|
||||||
|
: JSON.stringify(err.error || err)
|
||||||
|
if (errorMessage.includes(`Folder doesn't exist.`)) {
|
||||||
reject()
|
reject()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,6 +15,7 @@ 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'
|
||||||
@@ -133,6 +134,19 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export interface DcColumnSettings {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
39
client/src/app/shared/dc-validator/tests/isEmpty.spec.ts
Normal file
39
client/src/app/shared/dc-validator/tests/isEmpty.spec.ts
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { DcValidation } from '../models/dc-validation.model'
|
import { DcValidation } from '../models/dc-validation.model'
|
||||||
|
import { DQRule } from '../models/dq-rules.model'
|
||||||
|
import { getNotNullDefault } from './getNotNullDefault'
|
||||||
|
|
||||||
const schemaTypeMap: { [key: string]: any } = {
|
const schemaTypeMap: { [key: string]: any } = {
|
||||||
numeric: '',
|
numeric: '',
|
||||||
@@ -7,14 +9,25 @@ const schemaTypeMap: { [key: string]: any } = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema defines the default values for given types. For example when new row is added.
|
* Schema defines the default values for given types. For example when new row is added.
|
||||||
|
* Priority: NOTNULL RULE_VALUE > autocomplete first option > type default
|
||||||
*/
|
*/
|
||||||
export const getHotDataSchema = (
|
export function getHotDataSchema(
|
||||||
type: string | undefined,
|
type: string | undefined,
|
||||||
cellValidation?: DcValidation
|
cellValidation?: DcValidation,
|
||||||
): any => {
|
dqRules?: DQRule[]
|
||||||
|
): any {
|
||||||
|
// Check for NOTNULL default first
|
||||||
|
if (dqRules && cellValidation?.data) {
|
||||||
|
const defaultValue = getNotNullDefault(cellValidation.data, dqRules, type)
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!type) return schemaTypeMap.default
|
if (!type) return schemaTypeMap.default
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case 'dropdown':
|
||||||
case 'autocomplete': {
|
case 'autocomplete': {
|
||||||
return cellValidation && cellValidation.source
|
return cellValidation && cellValidation.source
|
||||||
? (cellValidation.source as string[] | number[])[0]
|
? (cellValidation.source as string[] | number[])[0]
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { DQRule } from '../models/dq-rules.model'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the NOTNULL default value for a column from DQ rules.
|
||||||
|
* Converts to number for numeric columns based on colType parameter.
|
||||||
|
*
|
||||||
|
* @param colName column name to look up
|
||||||
|
* @param dqRules array of DQ rules
|
||||||
|
* @param colType column type (e.g., 'numeric', 'text')
|
||||||
|
* @returns default value (string or number) if NOTNULL rule exists with non-empty value, otherwise undefined
|
||||||
|
*/
|
||||||
|
export function getNotNullDefault(
|
||||||
|
colName: string,
|
||||||
|
dqRules: DQRule[],
|
||||||
|
colType?: string
|
||||||
|
): string | number | undefined {
|
||||||
|
const notNullRule = dqRules.find(
|
||||||
|
(rule: DQRule) => rule.BASE_COL === colName && rule.RULE_TYPE === 'NOTNULL'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!notNullRule?.RULE_VALUE || notNullRule.RULE_VALUE.trim().length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colType === 'numeric' && !isNaN(Number(notNullRule.RULE_VALUE))) {
|
||||||
|
return Number(notNullRule.RULE_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
return notNullRule.RULE_VALUE
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user