Compare commits
	
		
			69 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
|  | c2e3b362e7 | ||
|  | 5d2d60d040 | ||
| 0e59f5406f | |||
|  | e7cb471c0b | ||
|  | 0465089207 | ||
|  | 4f2f59907c | ||
|  | 7d85328d41 | ||
| f2a9329196 | |||
|  | 7a8231615c | ||
|  | 0db6b25327 | ||
| e91f6f01a6 | |||
|  | 0b4042af60 | ||
|  | 519d8953b5 | ||
| 14a616fc1b | |||
| bfe5a8626f | |||
|  | 4ecd186e5c | 
| @@ -3,10 +3,21 @@ | |||||||
| # Using `--silent` helps for showing any errs in the first line of the response | # Using `--silent` helps for showing any errs in the first line of the response | ||||||
| # The first line is picked up by the VS Code GIT UI popup when rc is not 0 | # The first line is picked up by the VS Code GIT UI popup when rc is not 0 | ||||||
|  |  | ||||||
| if npm run --silent lint:silent ; then | if npm run --silent lint:check:silent ; then | ||||||
|     exit 0 |     exit 0 | ||||||
| else | else | ||||||
|     npm run --silent lint:fix |     npm run --silent lint:fix:silent | ||||||
|     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 | ||||||
|  | ) | ||||||
| @@ -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.14.0 |           node-version: 20.15.1 | ||||||
|  |  | ||||||
|       - 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.14.0 |           node-version: 20.15.1 | ||||||
|  |  | ||||||
|       - 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: [20.15.1] | ||||||
|  |  | ||||||
|  |     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 | ||||||
| @@ -80,7 +80,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 | ||||||
| @@ -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() | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -21,3 +21,4 @@ sasjsresults | |||||||
| .sasjsrc | .sasjsrc | ||||||
| client/.npmrc | client/.npmrc | ||||||
| *~ | *~ | ||||||
|  | .lighthouseci | ||||||
							
								
								
									
										87
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,90 @@ | |||||||
|  | ## [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) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * makedata vars ([e7cb471](https://git.datacontroller.io/dc/dc/commit/e7cb471c0b60058b03fe8cbed5e3e2e70dd72e26)) | ||||||
|  | * viya deploy makedata missing params ([7a82316](https://git.datacontroller.io/dc/dc/commit/7a8231615cb56710351fae5868e8fdeed54d180c)) | ||||||
|  |  | ||||||
|  | ## [7.0.2](https://git.datacontroller.io/dc/dc/compare/v7.0.1...v7.0.2) (2025-06-21) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * **viya deploy:** run makedata in new window to ensure logs are available for the user ([0b4042a](https://git.datacontroller.io/dc/dc/commit/0b4042af6011fdc65cfaaa5d4b1d8f48cd67f3b3)) | ||||||
|  |  | ||||||
|  | ## [7.0.1](https://git.datacontroller.io/dc/dc/compare/v7.0.0...v7.0.1) (2025-06-11) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * refresh process ([4ecd186](https://git.datacontroller.io/dc/dc/commit/4ecd186e5cb22dd436f2d7f1200956f4e3f27425)) | ||||||
|  |  | ||||||
| # [7.0.0](https://git.datacontroller.io/dc/dc/compare/v6.16.2...v7.0.0) (2025-06-11) | # [7.0.0](https://git.datacontroller.io/dc/dc/compare/v6.16.2...v7.0.0) (2025-06-11) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								README.md
									
									
									
									
									
								
							| @@ -39,3 +39,26 @@ For further information: | |||||||
| * 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`. | ||||||
| @@ -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') | ||||||
| @@ -399,11 +399,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 +428,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 +447,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 +463,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 +492,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 ( | ||||||
|   | |||||||
| @@ -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}`) |  | ||||||
| } |  | ||||||
| @@ -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;hyperformula@2.7.1;hyperformula@3.0.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) { | ||||||
|   | |||||||
							
								
								
									
										42
									
								
								client/lighthouserc.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								client/lighthouserc.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | 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' } | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										3164
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3164
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -31,7 +31,8 @@ | |||||||
|     "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" | ||||||
|   }, |   }, | ||||||
|   "private": true, |   "private": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
| @@ -48,8 +49,8 @@ | |||||||
|     "@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.12.2", | ||||||
|     "@sasjs/utils": "^3.4.0", |     "@sasjs/utils": "^3.4.0", | ||||||
|     "@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", | ||||||
| @@ -60,12 +61,12 @@ | |||||||
|     "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.26.0", | ||||||
|     "ngx-clipboard": "^16.0.0", |     "ngx-clipboard": "^16.0.0", | ||||||
| @@ -95,6 +96,7 @@ | |||||||
|     "@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", | ||||||
|   | |||||||
| @@ -78,6 +78,17 @@ export class AutomaticComponent implements OnInit { | |||||||
|     runMakeData: null |     runMakeData: null | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public sasjsConfig = this.sasService.getSasjsConfig() | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * makedata service will be run in a new window | ||||||
|  |    * This is needed to ensure that the user can see the logs | ||||||
|  |    * and the progress of the service execution. | ||||||
|  |    * If this is set to `false`, the service will be run in the same window | ||||||
|  |    * using the adapter request method. | ||||||
|  |    */ | ||||||
|  |   public deployInNewWindow: boolean = true | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     private eventService: EventService, |     private eventService: EventService, | ||||||
|     private deployService: DeployService, |     private deployService: DeployService, | ||||||
| @@ -156,8 +167,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 | ||||||
| @@ -177,7 +187,7 @@ export class AutomaticComponent implements OnInit { | |||||||
|           this.eventService.showAbortModal('admin groups', err) |           this.eventService.showAbortModal('admin groups', err) | ||||||
|  |  | ||||||
|           reject(err) |           reject(err) | ||||||
|         } |         }) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -246,7 +256,7 @@ export class AutomaticComponent implements OnInit { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async runAutoDeploy(executeJson: boolean = false) { |   public async runAutoDeploy(executeJson: boolean = false) { | ||||||
|     this.autodeploying = true |     if (!this.deployInNewWindow) this.autodeploying = true | ||||||
|  |  | ||||||
|     if (executeJson) { |     if (executeJson) { | ||||||
|       this.executeJson() |       this.executeJson() | ||||||
| @@ -255,7 +265,7 @@ export class AutomaticComponent implements OnInit { | |||||||
|     if (this.recreateDatabase) { |     if (this.recreateDatabase) { | ||||||
|       this.createDatabase() |       this.createDatabase() | ||||||
|     } else { |     } else { | ||||||
|       this.autodeployDone = true |       if (!this.deployInNewWindow) this.autodeployDone = true | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -296,51 +306,86 @@ export class AutomaticComponent implements OnInit { | |||||||
|       debug: true |       debug: true | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     this.sasJs |     if (this.deployInNewWindow) { | ||||||
|       .request(`services/admin/makedata`, data, overrideConfig, () => { |       this.runMakedataInNewWindow({ | ||||||
|         this.sasService.shouldLogin.next(true) |         contextName: selectedComputeContextName, | ||||||
|  |         admin: this.selectedAdminGroup, | ||||||
|  |         dcPath: this.dcPath | ||||||
|       }) |       }) | ||||||
|       .then((res: any) => { |     } else { | ||||||
|         this.autodeployDone = true |       this.sasJs | ||||||
|  |         .request(`services/admin/makedata`, data, overrideConfig, () => { | ||||||
|  |           this.sasService.shouldLogin.next(true) | ||||||
|  |         }) | ||||||
|  |         .then((res: any) => { | ||||||
|  |           this.autodeployDone = true | ||||||
|  |  | ||||||
|         try { |           try { | ||||||
|           this.makeDataResponse = JSON.stringify(res) |             this.makeDataResponse = JSON.stringify(res) | ||||||
|         } catch { |           } catch { | ||||||
|           this.makeDataResponse = res |             this.makeDataResponse = res | ||||||
|         } |           } | ||||||
|  |  | ||||||
|         if (res.result && res.result.length > 0) { |           if (res.result && res.result.length > 0) { | ||||||
|           this.autoDeployStatus.runMakeData = true |             this.autoDeployStatus.runMakeData = true | ||||||
|         } else { |           } else { | ||||||
|  |             this.autoDeployStatus.runMakeData = false | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           if (typeof res.sasjsAbort !== 'undefined') { | ||||||
|  |             const abortRes = res | ||||||
|  |             const abortMsg = abortRes.sasjsAbort[0].MSG | ||||||
|  |             const macMsg = abortRes.sasjsAbort[0].MAC | ||||||
|  |  | ||||||
|  |             this.eventService.showAbortModal('makedata', abortMsg, { | ||||||
|  |               SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT, | ||||||
|  |               SYSERRORTEXT: abortRes.SYSERRORTEXT, | ||||||
|  |               MAC: macMsg | ||||||
|  |             }) | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           if (this.helperService.isStreamingViya()) | ||||||
|  |             this.updateIndexHtmlComputeContext() | ||||||
|  |         }) | ||||||
|  |         .catch((err: any) => { | ||||||
|  |           this.eventService.showAbortModal('makedata', JSON.stringify(err)) | ||||||
|           this.autoDeployStatus.runMakeData = false |           this.autoDeployStatus.runMakeData = false | ||||||
|         } |           this.autodeployDone = true | ||||||
|  |  | ||||||
|         if (typeof res.sasjsAbort !== 'undefined') { |           try { | ||||||
|           const abortRes = res |             this.makeDataResponse = JSON.stringify(err) | ||||||
|           const abortMsg = abortRes.sasjsAbort[0].MSG |           } catch { | ||||||
|           const macMsg = abortRes.sasjsAbort[0].MAC |             this.makeDataResponse = err | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|           this.eventService.showAbortModal('makedata', abortMsg, { |   public runMakedataInNewWindow(params: { | ||||||
|             SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT, |     contextName: string | ||||||
|             SYSERRORTEXT: abortRes.SYSERRORTEXT, |     admin: string | ||||||
|             MAC: macMsg |     dcPath: string | ||||||
|           }) |   }) { | ||||||
|         } |     let serverUrl = this.sasjsConfig.serverUrl | ||||||
|  |     let appLoc = this.sasjsConfig.appLoc | ||||||
|  |     const execPath = this.sasService.getExecutionPath() | ||||||
|  |     let contextname = `&_contextname=${params.contextName}` | ||||||
|  |     let admin = `&admin=${params.admin}` | ||||||
|  |     let dcPath = `&dcpath=${params.dcPath}` | ||||||
|  |     let debug = `&_debug=131` | ||||||
|  |  | ||||||
|         if (this.helperService.isStreamingViya()) |     let programUrl = | ||||||
|           this.updateIndexHtmlComputeContext() |       serverUrl + | ||||||
|       }) |       execPath + | ||||||
|       .catch((err: any) => { |       '/?_program=' + | ||||||
|         this.eventService.showAbortModal('makedata', JSON.stringify(err)) |       appLoc + | ||||||
|         this.autoDeployStatus.runMakeData = false |       '/services/admin/makedata' + | ||||||
|         this.autodeployDone = true |       contextname + | ||||||
|  |       admin + | ||||||
|  |       dcPath + | ||||||
|  |       debug | ||||||
|  |  | ||||||
|         try { |     window.open(programUrl) | ||||||
|           this.makeDataResponse = JSON.stringify(err) |  | ||||||
|         } catch { |  | ||||||
|           this.makeDataResponse = err |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|   | |||||||
| @@ -171,23 +171,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() { | ||||||
|   | |||||||
| @@ -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' | ||||||
| @@ -71,13 +72,13 @@ import { ParseResult } from '../models/ParseResult.interface' | |||||||
|   }, |   }, | ||||||
|   encapsulation: ViewEncapsulation.None |   encapsulation: ViewEncapsulation.None | ||||||
| }) | }) | ||||||
| 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 +120,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 +176,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 +384,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 +397,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 +930,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 +961,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 +1024,54 @@ 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 | ||||||
|  |    */ | ||||||
|  |   private createEmptyRow(): any { | ||||||
|  |     const newRow: any = {} | ||||||
|  |     this.headerColumns.forEach((col: string) => { | ||||||
|  |       newRow[col] = '' | ||||||
|  |     }) | ||||||
|  |     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 +1151,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 +1535,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 +1572,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 +1601,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 +1754,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 +2307,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 +2332,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 +2391,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 +2606,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 | ||||||
|  |  | ||||||
| @@ -2410,6 +2727,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 +2806,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 +2869,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 +2947,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 +2971,21 @@ 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 | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     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 +3036,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, | ||||||
|   | |||||||
| @@ -52,6 +52,7 @@ export class LicensingComponent implements OnInit { | |||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     private route: ActivatedRoute, |     private route: ActivatedRoute, | ||||||
|  |     private router: Router, | ||||||
|     private licenceService: LicenceService, |     private licenceService: LicenceService, | ||||||
|     private sasService: SasService, |     private sasService: SasService, | ||||||
|     private appService: AppService |     private appService: AppService | ||||||
| @@ -124,7 +125,9 @@ export class LicensingComponent implements OnInit { | |||||||
|           res.adapterResponse.return[0] && |           res.adapterResponse.return[0] && | ||||||
|           res.adapterResponse.return[0].MSG === 'SUCCESS' |           res.adapterResponse.return[0].MSG === 'SUCCESS' | ||||||
|         ) { |         ) { | ||||||
|           location.replace(location.href.split('#')[0]) |           this.router.navigateByUrl('/').then(() => { | ||||||
|  |             window.location.reload() | ||||||
|  |           }) | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|       .finally(() => { |       .finally(() => { | ||||||
|   | |||||||
| @@ -746,28 +746,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 +780,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 +793,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() { | ||||||
|   | |||||||
| @@ -99,6 +99,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' | ||||||
| @@ -49,7 +50,7 @@ enum FileLoadingState { | |||||||
|   styleUrls: ['./multi-dataset.component.scss'], |   styleUrls: ['./multi-dataset.component.scss'], | ||||||
|   encapsulation: ViewEncapsulation.None |   encapsulation: ViewEncapsulation.None | ||||||
| }) | }) | ||||||
| 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 +90,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 +143,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 +185,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 +281,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 +443,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 +562,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 +637,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 +711,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' | ||||||
|   | |||||||
| @@ -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,32 @@ 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', | ||||||
| @@ -35,6 +62,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,55 @@ 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', | ||||||
| @@ -29,6 +78,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,22 @@ 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', | ||||||
| @@ -37,6 +54,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, | ||||||
|   | |||||||
| @@ -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() | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|   | |||||||
| @@ -386,27 +386,9 @@ | |||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <hot-table |       <hot-table | ||||||
|         *ngIf="viewboxTableIndex > -1" |         *ngIf="viewboxTableIndex > -1 && viewboxHotSettings.get(viewbox.id)" | ||||||
|         [hotId]="'hotInstance_viewbox_' + viewbox.id" |         [settings]="viewboxHotSettings.get(viewbox.id) || {}" | ||||||
|         id="hotTable" |         [id]="'hotTable_' + viewbox.id" | ||||||
|         className="htDark" |  | ||||||
|         [readOnly]="true" |  | ||||||
|         [modifyColWidth]="maxWidthCheker" |  | ||||||
|         [copyPaste]="viewboxTables[viewboxTableIndex].hotTable.copyPaste" |  | ||||||
|         [contextMenu]="viewboxTables[viewboxTableIndex].hotTable.contextMenu" |  | ||||||
|         [multiColumnSorting]="true" |  | ||||||
|         [viewportRowRenderingOffset]="50" |  | ||||||
|         [data]="viewboxTables[viewboxTableIndex].hotTable.data" |  | ||||||
|         [colHeaders]="viewboxTables[viewboxTableIndex].hotTable.colHeaders" |  | ||||||
|         [columns]="viewboxTables[viewboxTableIndex].hotTable.columns" |  | ||||||
|         [filters]="true" |  | ||||||
|         [dropdownMenu]="viewboxTables[viewboxTableIndex].hotTable.dropdownMenu" |  | ||||||
|         [height]="viewboxTables[viewboxTableIndex].hotTable.height" |  | ||||||
|         stretchH="all" |  | ||||||
|         [cells]="viewboxTables[viewboxTableIndex].hotTable.cells" |  | ||||||
|         [maxRows]="viewboxTables[viewboxTableIndex].hotTable.maxRows" |  | ||||||
|         [manualColumnResize]="true" |  | ||||||
|         [licenseKey]="viewboxTables[viewboxTableIndex].hotTable.licenseKey" |  | ||||||
|       ></hot-table> |       ></hot-table> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|   | |||||||
| @@ -21,9 +21,9 @@ import { | |||||||
|   ViewEncapsulation |   ViewEncapsulation | ||||||
| } from '@angular/core' | } from '@angular/core' | ||||||
| import { ActivatedRoute, Router } from '@angular/router' | import { ActivatedRoute, Router } from '@angular/router' | ||||||
| import { HotTableRegisterer } from '@handsontable/angular' |  | ||||||
| import { SASjsConfig } from '@sasjs/adapter' | import { SASjsConfig } from '@sasjs/adapter' | ||||||
| import Handsontable from 'handsontable' | import Handsontable from 'handsontable' | ||||||
|  | import { HotTableComponent } from '@handsontable/angular-wrapper' | ||||||
| import { cloneDeep } from 'lodash-es' | import { cloneDeep } from 'lodash-es' | ||||||
| import { Subscription } from 'rxjs' | import { Subscription } from 'rxjs' | ||||||
| import { FilterQuery, FilterGroup } from 'src/app/models/FilterQuery' | import { FilterQuery, FilterGroup } from 'src/app/models/FilterQuery' | ||||||
| @@ -54,6 +54,8 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|   @ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple |   @ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple | ||||||
|   @ViewChildren('dragHandleCorner') |   @ViewChildren('dragHandleCorner') | ||||||
|   dragHandleCornerQuery!: QueryList<ElementRef> |   dragHandleCornerQuery!: QueryList<ElementRef> | ||||||
|  |   @ViewChildren(HotTableComponent) | ||||||
|  |   hotTableComponents!: QueryList<HotTableComponent> | ||||||
|  |  | ||||||
|   private _viewboxModal: boolean = false |   private _viewboxModal: boolean = false | ||||||
|   get viewboxModal(): boolean { |   get viewboxModal(): boolean { | ||||||
| @@ -111,7 +113,7 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     }, |     }, | ||||||
|     columns: [], |     columns: [], | ||||||
|     cols: [], |     cols: [], | ||||||
|     height: '100%', |     height: 200, //WORKAROUND: Changed from '100%' to fixed pixel value because otherwize hot does not load | ||||||
|     settings: {}, |     settings: {}, | ||||||
|     hiddenColumns: true, |     hiddenColumns: true, | ||||||
|     manualColumnMove: false, |     manualColumnMove: false, | ||||||
| @@ -119,8 +121,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     licenseKey: undefined, |     licenseKey: undefined, | ||||||
|     dropdownMenu: undefined |     dropdownMenu: undefined | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public viewboxHotSettings: Map<number, Handsontable.GridSettings> = new Map() | ||||||
|   public viewboxTables: ViewboxTable[] = [] |   public viewboxTables: ViewboxTable[] = [] | ||||||
|   private hotTableRegisterer: HotTableRegisterer |  | ||||||
|  |  | ||||||
|   public filteringViewbox: Viewbox | undefined |   public filteringViewbox: Viewbox | undefined | ||||||
|  |  | ||||||
| @@ -150,9 +153,7 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     private router: Router, |     private router: Router, | ||||||
|     private activatedRoute: ActivatedRoute, |     private activatedRoute: ActivatedRoute, | ||||||
|     private cdf: ChangeDetectorRef |     private cdf: ChangeDetectorRef | ||||||
|   ) { |   ) {} | ||||||
|     this.hotTableRegisterer = new HotTableRegisterer() |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     // Load libraries |     // Load libraries | ||||||
| @@ -207,7 +208,17 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   ngAfterViewInit(): void { |   ngAfterViewInit(): void { | ||||||
|     //set handles for box resize |     // Set handles for box resize and ensure HOT components are properly initialized | ||||||
|  |     setTimeout(() => { | ||||||
|  |       this.setAllHandleTransform() | ||||||
|  |  | ||||||
|  |       // Force refresh of any existing HOT instances after view init | ||||||
|  |       this.viewboxes.forEach((viewbox) => { | ||||||
|  |         if (this.getViewboxTableIndex(viewbox) > -1) { | ||||||
|  |           this.refreshTableAfterResize(viewbox) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }, 1000) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Maximum number of open viewboxes reached |   // Maximum number of open viewboxes reached | ||||||
| @@ -304,6 +315,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|           if (viewboxTable) { |           if (viewboxTable) { | ||||||
|             viewboxTable.hotTable.data = res.viewdata |             viewboxTable.hotTable.data = res.viewdata | ||||||
|  |  | ||||||
|  |             // Update settings with new data | ||||||
|  |             this.createViewboxTableSettings(viewbox) | ||||||
|  |  | ||||||
|             resolve(null) |             resolve(null) | ||||||
|           } else { |           } else { | ||||||
|             resolve(null) |             resolve(null) | ||||||
| @@ -413,6 +427,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|           viewbox.query = this.helperService.deepClone(res.query) |           viewbox.query = this.helperService.deepClone(res.query) | ||||||
|           viewbox.filterText = res.sasparams[0].FILTER_TEXT |           viewbox.filterText = res.sasparams[0].FILTER_TEXT | ||||||
|  |  | ||||||
|  |           // Create settings for this viewbox | ||||||
|  |           this.createViewboxTableSettings(viewbox) | ||||||
|  |  | ||||||
|           setTimeout(() => { |           setTimeout(() => { | ||||||
|             this.updateHotColumns( |             this.updateHotColumns( | ||||||
|               viewboxTable!.hotTable.colHeadersHidden || [], |               viewboxTable!.hotTable.colHeadersHidden || [], | ||||||
| @@ -421,30 +438,34 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|  |  | ||||||
|             // HOT Settings are bound in HTML but some settings due to timing issues |             // HOT Settings are bound in HTML but some settings due to timing issues | ||||||
|             // requires to be updated after the HOT is instanced |             // requires to be updated after the HOT is instanced | ||||||
|             // after the update `render` method is called |             // Use a longer timeout to ensure the HOT component is fully initialized | ||||||
|             const hotInstance = this.getViewboxHotInstance(viewbox.id) |             setTimeout(() => { | ||||||
|  |               const hotInstance = this.getViewboxHotInstance(viewbox.id) | ||||||
|  |  | ||||||
|             hotInstance?.updateSettings({ |               if (hotInstance) { | ||||||
|               manualColumnMove: viewboxTable!.hotTable.manualColumnMove, |                 hotInstance.updateSettings({ | ||||||
|               afterGetColHeader: (col: number, th: any) => { |                   manualColumnMove: viewboxTable!.hotTable.manualColumnMove, | ||||||
|                 const column = hotInstance?.colToProp(col) as string |                   afterGetColHeader: (col: number, th: any) => { | ||||||
|  |                     const column = hotInstance?.colToProp(col) as string | ||||||
|  |  | ||||||
|                 // header columns styling - primary keys |                     // header columns styling - primary keys | ||||||
|                 const isPKCol = |                     const isPKCol = | ||||||
|                   column && |                       column && | ||||||
|                   viewboxTable!.hotTable.headerPks.indexOf(column) > -1 |                       viewboxTable!.hotTable.headerPks.indexOf(column) > -1 | ||||||
|  |  | ||||||
|                 if (isPKCol) th.classList.add('primaryKeyHeaderStyle') |                     if (isPKCol) th.classList.add('primaryKeyHeaderStyle') | ||||||
|                 // Dark mode |                     // Dark mode | ||||||
|                 th.classList.add(globals.handsontable.darkTableHeaderClass) |                     th.classList.add(globals.handsontable.darkTableHeaderClass) | ||||||
|  |                   } | ||||||
|  |                 }) | ||||||
|  |                 hotInstance.render() | ||||||
|               } |               } | ||||||
|             }) |  | ||||||
|             hotInstance?.render() |  | ||||||
|  |  | ||||||
|             if (this.selectedViewbox) { |               if (this.selectedViewbox) { | ||||||
|               this.resetSelectedViewbox(viewbox) |                 this.resetSelectedViewbox(viewbox) | ||||||
|             } |               } | ||||||
|           }) |             }, 500) | ||||||
|  |           }, 100) | ||||||
|  |  | ||||||
|           resolve() |           resolve() | ||||||
|         }) |         }) | ||||||
| @@ -490,6 +511,68 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     return index |     return index | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Create and store Handsontable settings for a specific viewbox | ||||||
|  |    * @param viewbox | ||||||
|  |    */ | ||||||
|  |   private createViewboxTableSettings(viewbox: Viewbox): void { | ||||||
|  |     const viewboxTableIndex = this.getViewboxTableIndex(viewbox) | ||||||
|  |     if (viewboxTableIndex === -1) { | ||||||
|  |       this.viewboxHotSettings.set(viewbox.id, {}) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const viewboxTable = this.viewboxTables[viewboxTableIndex] | ||||||
|  |     const calculatedHeight = this.calculateTableHeight(viewbox) | ||||||
|  |  | ||||||
|  |     // HOT v16 settings - data will be loaded manually after initialization | ||||||
|  |     const settings: Handsontable.GridSettings = { | ||||||
|  |       colHeaders: viewboxTable.hotTable.colHeaders, | ||||||
|  |       columns: viewboxTable.hotTable.columns, | ||||||
|  |       height: calculatedHeight, | ||||||
|  |       readOnly: true, | ||||||
|  |       modifyColWidth: this.maxWidthCheker, | ||||||
|  |       copyPaste: viewboxTable.hotTable.copyPaste, | ||||||
|  |       contextMenu: viewboxTable.hotTable.contextMenu, | ||||||
|  |       multiColumnSorting: true, | ||||||
|  |       viewportRowRenderingOffset: 50, | ||||||
|  |       filters: true, | ||||||
|  |       dropdownMenu: viewboxTable.hotTable.dropdownMenu, | ||||||
|  |       stretchH: 'all', | ||||||
|  |       cells: viewboxTable.hotTable.cells, | ||||||
|  |       maxRows: viewboxTable.hotTable.maxRows || Infinity, | ||||||
|  |       manualColumnResize: true, | ||||||
|  |       rowHeaders: true, | ||||||
|  |       licenseKey: viewboxTable.hotTable.licenseKey | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Force a new object reference to trigger change detection | ||||||
|  |     this.viewboxHotSettings.set(viewbox.id, { ...settings }) | ||||||
|  |  | ||||||
|  |     // Force change detection to ensure the template updates | ||||||
|  |     setTimeout(() => { | ||||||
|  |       this.cdf.detectChanges() | ||||||
|  |  | ||||||
|  |       // Try to get the HOT instance and force a render | ||||||
|  |       setTimeout(() => { | ||||||
|  |         const hotInstance = this.getViewboxHotInstance(viewbox.id) | ||||||
|  |         if (hotInstance) { | ||||||
|  |           // Load data manually - this is required for HOT v16 Angular wrapper | ||||||
|  |           hotInstance.loadData(viewboxTable.hotTable.data) | ||||||
|  |           hotInstance.render() | ||||||
|  |         } | ||||||
|  |       }, 500) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get stored Handsontable settings for a specific viewbox | ||||||
|  |    * @param viewbox | ||||||
|  |    */ | ||||||
|  |   getViewboxTableSettings(viewbox: Viewbox): Handsontable.GridSettings { | ||||||
|  |     return this.viewboxHotSettings.get(viewbox.id) || {} | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Viewbox resize |    * Viewbox resize | ||||||
|    * @param dragHandle |    * @param dragHandle | ||||||
| @@ -513,6 +596,12 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     this.helperService.debounceCall(1000, () => { |     this.helperService.debounceCall(1000, () => { | ||||||
|       this.viewboxChanged() |       this.viewboxChanged() | ||||||
|       this.eventService.dispatchEvent('resize') |       this.eventService.dispatchEvent('resize') | ||||||
|  |  | ||||||
|  |       // Refresh all viewbox tables after resize and update their settings | ||||||
|  |       this.viewboxes.forEach((viewbox) => { | ||||||
|  |         // Settings will include updated heights when accessed | ||||||
|  |         this.refreshTableAfterResize(viewbox) | ||||||
|  |       }) | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
| @@ -672,6 +761,12 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|  |  | ||||||
|     setTimeout(() => { |     setTimeout(() => { | ||||||
|       this.setAllHandleTransform() |       this.setAllHandleTransform() | ||||||
|  |  | ||||||
|  |       // Refresh all tables after snap to grid | ||||||
|  |       this.viewboxes.forEach((viewbox) => { | ||||||
|  |         // Settings will include correct height when accessed | ||||||
|  |         this.refreshTableAfterResize(viewbox) | ||||||
|  |       }) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -713,6 +808,12 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     viewbox.minimized = false |     viewbox.minimized = false | ||||||
|  |  | ||||||
|     this.viewboxChanged() |     this.viewboxChanged() | ||||||
|  |  | ||||||
|  |     // Refresh table after restoring | ||||||
|  |     setTimeout(() => { | ||||||
|  |       // Settings will include correct height when accessed | ||||||
|  |       this.refreshTableAfterResize(viewbox) | ||||||
|  |     }, 100) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   collapse(viewbox: Viewbox) { |   collapse(viewbox: Viewbox) { | ||||||
| @@ -723,6 +824,12 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|   expand(viewbox: Viewbox) { |   expand(viewbox: Viewbox) { | ||||||
|     viewbox.collapsed = false |     viewbox.collapsed = false | ||||||
|     this.viewboxChanged() |     this.viewboxChanged() | ||||||
|  |  | ||||||
|  |     // Refresh table after expanding | ||||||
|  |     setTimeout(() => { | ||||||
|  |       // Settings will include correct height when accessed | ||||||
|  |       this.refreshTableAfterResize(viewbox) | ||||||
|  |     }, 100) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -739,6 +846,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     if (index > -1) this.viewboxes.splice(index, 1) |     if (index > -1) this.viewboxes.splice(index, 1) | ||||||
|     if (viewtableIndex > -1) this.viewboxTables.splice(viewtableIndex, 1) |     if (viewtableIndex > -1) this.viewboxTables.splice(viewtableIndex, 1) | ||||||
|  |  | ||||||
|  |     // Clean up settings for this viewbox | ||||||
|  |     this.viewboxHotSettings.delete(viewbox.id) | ||||||
|  |  | ||||||
|     if (this.selectedViewbox?.id === viewbox.id) { |     if (this.selectedViewbox?.id === viewbox.id) { | ||||||
|       this.unsetSelectedViewbox() |       this.unsetSelectedViewbox() | ||||||
|     } |     } | ||||||
| @@ -1036,6 +1146,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         viewboxTable.hotTable.data = res.viewdata |         viewboxTable.hotTable.data = res.viewdata | ||||||
|  |  | ||||||
|  |         // Update settings with new data | ||||||
|  |         this.createViewboxTableSettings(viewbox) | ||||||
|       }) |       }) | ||||||
|       .catch((err: any) => { |       .catch((err: any) => { | ||||||
|         this.loggerService.error(err) |         this.loggerService.error(err) | ||||||
| @@ -1064,6 +1177,8 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     this.updateHiddenColumnsHot(hiddenColProps, viewboxId) |     this.updateHiddenColumnsHot(hiddenColProps, viewboxId) | ||||||
|  |  | ||||||
|     this.setColumnOrder(viewboxId) |     this.setColumnOrder(viewboxId) | ||||||
|  |  | ||||||
|  |     // Settings will be regenerated when accessed | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -1158,19 +1273,87 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Calculate available height for Handsontable | ||||||
|  |    * @param viewbox The viewbox to calculate height for | ||||||
|  |    * @returns Available height in pixels | ||||||
|  |    */ | ||||||
|  |   calculateTableHeight(viewbox: Viewbox): number { | ||||||
|  |     // Calculate the exact height of the content div | ||||||
|  |     const dragHandleHeight = 20 | ||||||
|  |     const searchFormHeight = 36 | ||||||
|  |     const padding = 2 | ||||||
|  |     // Return the exact remaining height for the table with minimum height | ||||||
|  |     const calculatedHeight = | ||||||
|  |       viewbox.height - dragHandleHeight - searchFormHeight - padding | ||||||
|  |  | ||||||
|  |     return calculatedHeight | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Refresh Handsontable instance after resize | ||||||
|  |    * @param viewbox The viewbox to refresh | ||||||
|  |    */ | ||||||
|  |   refreshTableAfterResize(viewbox: Viewbox): void { | ||||||
|  |     const hotInstance = this.getViewboxHotInstance(viewbox.id) | ||||||
|  |     if (hotInstance) { | ||||||
|  |       // Force the table to recalculate its dimensions | ||||||
|  |       setTimeout(() => { | ||||||
|  |         try { | ||||||
|  |           // Update height setting and refresh | ||||||
|  |           hotInstance.updateSettings({ | ||||||
|  |             height: this.calculateTableHeight(viewbox) | ||||||
|  |           }) | ||||||
|  |           hotInstance.refreshDimensions() | ||||||
|  |           hotInstance.render() | ||||||
|  |         } catch (error) { | ||||||
|  |           // If refresh fails, try again later | ||||||
|  |           setTimeout(() => { | ||||||
|  |             try { | ||||||
|  |               hotInstance.updateSettings({ | ||||||
|  |                 height: this.calculateTableHeight(viewbox) | ||||||
|  |               }) | ||||||
|  |               hotInstance.refreshDimensions() | ||||||
|  |             } catch (e) { | ||||||
|  |               console.warn( | ||||||
|  |                 'Failed to refresh HOT dimensions for viewbox', | ||||||
|  |                 viewbox.id, | ||||||
|  |                 e | ||||||
|  |               ) | ||||||
|  |             } | ||||||
|  |           }, 500) | ||||||
|  |         } | ||||||
|  |       }, 100) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * |    * | ||||||
|    * @param viewboxId |    * @param viewboxId | ||||||
|    * @returns HOT Instance from the given Viewbox |    * @returns HOT Instance from the given Viewbox | ||||||
|    */ |    */ | ||||||
|   private getViewboxHotInstance(viewboxId?: number): Handsontable | undefined { |   private getViewboxHotInstance(viewboxId?: number): Handsontable | undefined { | ||||||
|     if (!viewboxId) return |     if (!viewboxId || !this.hotTableComponents) return | ||||||
|  |  | ||||||
|     const hotInstance = this.hotTableRegisterer.getInstance( |     // Find the component corresponding to this viewbox | ||||||
|       `hotInstance_viewbox_${viewboxId}` |     // Since we only show one table per viewbox and they're rendered in order, | ||||||
|     ) |     // we can match by the viewbox's position in the array | ||||||
|  |     const viewboxIndex = this.viewboxes.findIndex((vb) => vb.id === viewboxId) | ||||||
|  |     if (viewboxIndex === -1) return | ||||||
|  |  | ||||||
|     return hotInstance |     // Get the HOT component at this index | ||||||
|  |     const hotComponents = this.hotTableComponents.toArray() | ||||||
|  |     let hotComponentIndex = 0 | ||||||
|  |  | ||||||
|  |     // Count how many viewboxes before this one have loaded tables | ||||||
|  |     for (let i = 0; i < viewboxIndex; i++) { | ||||||
|  |       if (this.getViewboxTableIndex(this.viewboxes[i]) > -1) { | ||||||
|  |         hotComponentIndex++ | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const hotTableComponent = hotComponents[hotComponentIndex] | ||||||
|  |     return hotTableComponent?.hotInstance || undefined | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|   | |||||||
| @@ -4,7 +4,11 @@ import { ClarityModule } from '@clr/angular' | |||||||
| import { FormsModule } from '@angular/forms' | import { FormsModule } from '@angular/forms' | ||||||
| import { ViewboxesComponent } from './viewboxes.component' | import { ViewboxesComponent } from './viewboxes.component' | ||||||
| import { QueryModule } from 'src/app/query/query.module' | import { QueryModule } from 'src/app/query/query.module' | ||||||
| import { HotTableModule } from '@handsontable/angular' | import { HotTableModule } from '@handsontable/angular-wrapper' | ||||||
|  | import { registerAllModules } from 'handsontable/registry' | ||||||
|  |  | ||||||
|  | // register Handsontable's modules | ||||||
|  | registerAllModules() | ||||||
| import { DragDropModule } from '@angular/cdk/drag-drop' | import { DragDropModule } from '@angular/cdk/drag-drop' | ||||||
| import { AutocompleteModule } from '../autocomplete/autocomplete.module' | import { AutocompleteModule } from '../autocomplete/autocomplete.module' | ||||||
| import { DcTreeModule } from '../dc-tree/dc-tree.module' | import { DcTreeModule } from '../dc-tree/dc-tree.module' | ||||||
|   | |||||||
| @@ -46,6 +46,10 @@ | |||||||
|                 rejected: tableDetails?.REVIEW_STATUS_ID === 'REJECTED', |                 rejected: tableDetails?.REVIEW_STATUS_ID === 'REJECTED', | ||||||
|                 accepted: tableDetails?.REVIEW_STATUS_ID === 'APPROVED' |                 accepted: tableDetails?.REVIEW_STATUS_ID === 'APPROVED' | ||||||
|               }" |               }" | ||||||
|  |               [attr.aria-label]=" | ||||||
|  |                 'Review status: ' + tableDetails?.REVIEW_STATUS_ID | ||||||
|  |               " | ||||||
|  |               role="status" | ||||||
|             > |             > | ||||||
|               {{ tableDetails?.REVIEW_STATUS_ID }} |               {{ tableDetails?.REVIEW_STATUS_ID }} | ||||||
|             </span> |             </span> | ||||||
| @@ -61,6 +65,7 @@ | |||||||
|                 class="btn btn-sm btn-outline text-center mr-5i" |                 class="btn btn-sm btn-outline text-center mr-5i" | ||||||
|                 (click)="viewerTableScreen()" |                 (click)="viewerTableScreen()" | ||||||
|                 [disabled]="revertingChanges" |                 [disabled]="revertingChanges" | ||||||
|  |                 aria-label="View base table" | ||||||
|               > |               > | ||||||
|                 View base table |                 View base table | ||||||
|               </button> |               </button> | ||||||
| @@ -74,6 +79,7 @@ | |||||||
|                 " |                 " | ||||||
|                 (click)="approveTableScreen()" |                 (click)="approveTableScreen()" | ||||||
|                 [disabled]="revertingChanges" |                 [disabled]="revertingChanges" | ||||||
|  |                 aria-label="Approve table" | ||||||
|               > |               > | ||||||
|                 Approve |                 Approve | ||||||
|               </button> |               </button> | ||||||
| @@ -81,14 +87,16 @@ | |||||||
|                 class="btn btn-sm btn-info-outline text-center mr-5i" |                 class="btn btn-sm btn-info-outline text-center mr-5i" | ||||||
|                 (click)="goBack()" |                 (click)="goBack()" | ||||||
|                 [disabled]="revertingChanges" |                 [disabled]="revertingChanges" | ||||||
|  |                 aria-label="Edit base table" | ||||||
|               > |               > | ||||||
|                 Edit base table |                 Edit base table | ||||||
|               </button> |               </button> | ||||||
|               <button |               <button | ||||||
|                 class="btn btn-sm btn-success text-center mr-5i min-w-0" |                 class="btn btn-sm btn-success text-center mr-5i min-w-0" | ||||||
|                 (click)="download(tableDetails?.TABLE_ID)" |                 (click)="download(tableDetails?.TABLE_ID)" | ||||||
|  |                 aria-label="Download audit file" | ||||||
|               > |               > | ||||||
|                 <clr-icon shape="download"></clr-icon> |                 <clr-icon shape="download" aria-hidden="true"></clr-icon> | ||||||
|               </button> |               </button> | ||||||
|  |  | ||||||
|               <clr-tooltip> |               <clr-tooltip> | ||||||
| @@ -98,6 +106,7 @@ | |||||||
|                   clrTooltipTrigger |                   clrTooltipTrigger | ||||||
|                   [clrLoading]="revertingChanges" |                   [clrLoading]="revertingChanges" | ||||||
|                   class="btn btn-sm btn-danger text-center mt-20" |                   class="btn btn-sm btn-danger text-center mt-20" | ||||||
|  |                   aria-label="Revert this and all subsequent changes" | ||||||
|                 > |                 > | ||||||
|                   REVERT |                   REVERT | ||||||
|  |  | ||||||
| @@ -116,18 +125,10 @@ | |||||||
|       </div> |       </div> | ||||||
|       <div class="card-block"> |       <div class="card-block"> | ||||||
|         <hot-table |         <hot-table | ||||||
|           hotId="hotInstance" |  | ||||||
|           id="hotTable" |           id="hotTable" | ||||||
|           className="htDark" |  | ||||||
|           [data]="hotTable.data" |           [data]="hotTable.data" | ||||||
|           [colHeaders]="hotTable.colHeaders" |           [settings]="hotTableSettings" | ||||||
|           [columns]="hotTable.columns" |           aria-label="Staged data table" | ||||||
|           [maxRows]="hotTable.maxRows" |  | ||||||
|           [height]="hotTable.height" |  | ||||||
|           [licenseKey]="hotTable.licenseKey" |  | ||||||
|           [afterGetColHeader]="hotTable.afterGetColHeader" |  | ||||||
|           stretchH="all" |  | ||||||
|           [cells]="hotTable.cells" |  | ||||||
|         > |         > | ||||||
|           <!--[licenseKey]=null--> |           <!--[licenseKey]=null--> | ||||||
|         </hot-table> |         </hot-table> | ||||||
|   | |||||||
| @@ -1,10 +1,17 @@ | |||||||
| import { Component, OnInit, ViewEncapsulation } from '@angular/core' | import { | ||||||
|  |   Component, | ||||||
|  |   OnInit, | ||||||
|  |   ViewEncapsulation, | ||||||
|  |   ViewChild, | ||||||
|  |   AfterViewInit | ||||||
|  | } from '@angular/core' | ||||||
| import { SasStoreService } from '../services/sas-store.service' | import { SasStoreService } from '../services/sas-store.service' | ||||||
| import { Router } from '@angular/router' | import { Router } from '@angular/router' | ||||||
| import { ActivatedRoute } from '@angular/router' | import { ActivatedRoute } 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 { HotTableInterface } from '../models/HotTable.interface' | import { HotTableInterface } from '../models/HotTable.interface' | ||||||
|  | import Handsontable from 'handsontable' | ||||||
| import { LicenceService } from '../services/licence.service' | import { LicenceService } from '../services/licence.service' | ||||||
| import { globals } from '../_globals' | import { globals } from '../_globals' | ||||||
| import { EditorsRestoreServiceResponse } from '../models/sas/editors-restore.model' | import { EditorsRestoreServiceResponse } from '../models/sas/editors-restore.model' | ||||||
| @@ -19,7 +26,7 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper | |||||||
|   }, |   }, | ||||||
|   encapsulation: ViewEncapsulation.None |   encapsulation: ViewEncapsulation.None | ||||||
| }) | }) | ||||||
| export class StageComponent implements OnInit { | export class StageComponent implements OnInit, AfterViewInit { | ||||||
|   public table_id: any |   public table_id: any | ||||||
|   public jsParams: any |   public jsParams: any | ||||||
|   public keysArray: any |   public keysArray: any | ||||||
| @@ -32,12 +39,42 @@ export class StageComponent implements OnInit { | |||||||
|     colHeaders: [], |     colHeaders: [], | ||||||
|     columns: [], |     columns: [], | ||||||
|     height: 500, |     height: 500, | ||||||
|     settings: {}, |     settings: { | ||||||
|  |       // Disable problematic ARIA attributes that cause accessibility issues | ||||||
|  |       ariaTags: false, | ||||||
|  |       // Use grid role instead of treegrid for better accessibility | ||||||
|  |       tableClassName: 'htCenter', | ||||||
|  |       // Disable focus management to avoid focus catcher issues | ||||||
|  |       outsideClickDeselects: false, | ||||||
|  |       // Use simpler accessibility mode | ||||||
|  |       autoWrapRow: false, | ||||||
|  |       autoWrapCol: false | ||||||
|  |     }, | ||||||
|     licenseKey: undefined, |     licenseKey: undefined, | ||||||
|     maxRows: this.licenceState.value.stage_rows_allowed || Infinity, |     maxRows: this.licenceState.value.stage_rows_allowed || Infinity, | ||||||
|     afterGetColHeader: (column, th, headerLevel) => { |     afterGetColHeader: (column, th, headerLevel) => { | ||||||
|       // Dark mode |       // Dark mode | ||||||
|       th.classList.add(globals.handsontable.darkTableHeaderClass) |       th.classList.add(globals.handsontable.darkTableHeaderClass) | ||||||
|  |     }, | ||||||
|  |     afterInit: () => { | ||||||
|  |       // Fix accessibility issues with focus catcher inputs | ||||||
|  |       this.fixFocusCatcherAccessibility() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get hotTableSettings(): Handsontable.GridSettings { | ||||||
|  |     return { | ||||||
|  |       ...this.hotTable.settings, | ||||||
|  |       colHeaders: this.hotTable.colHeaders, | ||||||
|  |       columns: this.hotTable.columns, | ||||||
|  |       maxRows: this.hotTable.maxRows, | ||||||
|  |       height: this.hotTable.height, | ||||||
|  |       licenseKey: this.hotTable.licenseKey, | ||||||
|  |       afterGetColHeader: this.hotTable.afterGetColHeader, | ||||||
|  |       afterInit: this.hotTable.afterInit, | ||||||
|  |       stretchH: 'all', | ||||||
|  |       cells: this.hotTable.cells, | ||||||
|  |       className: 'htDark' | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -169,6 +206,13 @@ export class StageComponent implements OnInit { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   ngAfterViewInit() { | ||||||
|  |     // Additional accessibility fixes after view is initialized | ||||||
|  |     setTimeout(() => { | ||||||
|  |       this.fixFocusCatcherAccessibility() | ||||||
|  |     }, 500) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   revertChanges() { |   revertChanges() { | ||||||
|     this.revertingChanges = true |     this.revertingChanges = true | ||||||
|  |  | ||||||
| @@ -204,4 +248,27 @@ export class StageComponent implements OnInit { | |||||||
|       } |       } | ||||||
|     }, 200) |     }, 200) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private fixFocusCatcherAccessibility() { | ||||||
|  |     // Add labels to focus catcher inputs to fix accessibility issues | ||||||
|  |     setTimeout(() => { | ||||||
|  |       const focusCatchers = document.querySelectorAll('.htFocusCatcher') | ||||||
|  |       focusCatchers.forEach((input: any, index: number) => { | ||||||
|  |         if (input) { | ||||||
|  |           // Add proper accessibility attributes | ||||||
|  |           input.setAttribute('aria-label', `Table focus catcher ${index + 1}`) | ||||||
|  |           input.setAttribute('aria-hidden', 'true') | ||||||
|  |           input.setAttribute('tabindex', '-1') | ||||||
|  |           input.setAttribute('role', 'presentation') | ||||||
|  |           // Add a hidden label element | ||||||
|  |           const label = document.createElement('label') | ||||||
|  |           label.setAttribute('for', input.id || `htFocusCatcher${index}`) | ||||||
|  |           label.setAttribute('aria-hidden', 'true') | ||||||
|  |           label.style.display = 'none' | ||||||
|  |           label.textContent = `Table focus catcher ${index + 1}` | ||||||
|  |           input.parentNode?.insertBefore(label, input) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }, 100) | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { NgModule } from '@angular/core' | import { NgModule } from '@angular/core' | ||||||
| import { CommonModule } from '@angular/common' | import { CommonModule } from '@angular/common' | ||||||
| import { StageComponent } from './stage.component' | import { StageComponent } from './stage.component' | ||||||
| import { HotTableModule } from '@handsontable/angular' | import { HotTableModule } from '@handsontable/angular-wrapper' | ||||||
| import { ClarityModule } from '@clr/angular' | import { ClarityModule } from '@clr/angular' | ||||||
| import { RouterModule, Routes } from '@angular/router' | import { RouterModule, Routes } from '@angular/router' | ||||||
|  |  | ||||||
|   | |||||||
| @@ -621,32 +621,16 @@ | |||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div *ngIf="!noData && !noDataReqErr && table" class="clr-flex-1"> |     <div *ngIf="!noData && !noDataReqErr && table" class="clr-flex-1"> | ||||||
|       <hot-table |       <div class="hot-wrapper clr-flex-1"> | ||||||
|         hotId="hotInstance" |         <hot-table | ||||||
|         id="hotTable" |           #hotInstance | ||||||
|         className="htDark" |           id="hotTable" | ||||||
|         [multiColumnSorting]="true" |           class="view-hot" | ||||||
|         [viewportRowRenderingOffset]="50" |           [data]="hotTable.data" | ||||||
|         [data]="hotTable.data" |           [settings]="hotTableSettings" | ||||||
|         [colHeaders]="hotTable.colHeaders" |         > | ||||||
|         [columns]="hotTable.columns" |         </hot-table> | ||||||
|         [copyPaste]="hotTable.copyPaste" |       </div> | ||||||
|         [contextMenu]="hotTable.contextMenu" |  | ||||||
|         [filters]="true" |  | ||||||
|         [dropdownMenu]="hotTable.dropdownMenu" |  | ||||||
|         [height]="hotTable.height" |  | ||||||
|         stretchH="all" |  | ||||||
|         [modifyColWidth]="maxWidthCheker" |  | ||||||
|         [cells]="hotTable.cells" |  | ||||||
|         [maxRows]="hotTable.maxRows" |  | ||||||
|         [manualColumnResize]="true" |  | ||||||
|         [afterGetColHeader]="hotTable.afterGetColHeader" |  | ||||||
|         [rowHeaders]="hotTable.rowHeaders" |  | ||||||
|         [rowHeaderWidth]="hotTable.rowHeaderWidth" |  | ||||||
|         [rowHeights]="hotTable.rowHeights" |  | ||||||
|         [licenseKey]="hotTable.licenseKey" |  | ||||||
|       > |  | ||||||
|       </hot-table> |  | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div> |     <div> | ||||||
|   | |||||||
| @@ -3,9 +3,11 @@ import { | |||||||
|   AfterContentInit, |   AfterContentInit, | ||||||
|   ChangeDetectorRef, |   ChangeDetectorRef, | ||||||
|   AfterViewInit, |   AfterViewInit, | ||||||
|  |   OnDestroy, | ||||||
|   ViewChildren, |   ViewChildren, | ||||||
|   QueryList, |   QueryList, | ||||||
|   ViewEncapsulation |   ViewEncapsulation, | ||||||
|  |   ViewChild | ||||||
| } from '@angular/core' | } from '@angular/core' | ||||||
| import { SasStoreService } from '../services/sas-store.service' | import { SasStoreService } from '../services/sas-store.service' | ||||||
| import { Subscription } from 'rxjs' | import { Subscription } from 'rxjs' | ||||||
| @@ -16,7 +18,7 @@ import { globals } from '../_globals' | |||||||
|  |  | ||||||
| import { EventService } from '../services/event.service' | import { EventService } from '../services/event.service' | ||||||
| import { HelperService } from '../services/helper.service' | import { HelperService } from '../services/helper.service' | ||||||
| import { HotTableRegisterer } from '@handsontable/angular' | import { HotTableComponent } from '@handsontable/angular-wrapper' | ||||||
| import { SasService } from '../services/sas.service' | import { SasService } from '../services/sas.service' | ||||||
| import { SASjsConfig } from '@sasjs/adapter' | import { SASjsConfig } from '@sasjs/adapter' | ||||||
| import { QueryComponent } from '../query/query.component' | import { QueryComponent } from '../query/query.component' | ||||||
| @@ -49,10 +51,15 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper | |||||||
|   }, |   }, | ||||||
|   encapsulation: ViewEncapsulation.None |   encapsulation: ViewEncapsulation.None | ||||||
| }) | }) | ||||||
| export class ViewerComponent implements AfterContentInit, AfterViewInit { | export class ViewerComponent | ||||||
|  |   implements AfterContentInit, AfterViewInit, OnDestroy | ||||||
|  | { | ||||||
|   @ViewChildren('queryFilter') |   @ViewChildren('queryFilter') | ||||||
|   queryFilterCompList: QueryList<QueryComponent> = new QueryList() |   queryFilterCompList: QueryList<QueryComponent> = new QueryList() | ||||||
|  |  | ||||||
|  |   @ViewChild('hotInstance', { static: false }) | ||||||
|  |   hotInstanceViewChild!: Handsontable | ||||||
|  |  | ||||||
|   public libraries!: Array<any> |   public libraries!: Array<any> | ||||||
|   public librariesPaging: boolean = false |   public librariesPaging: boolean = false | ||||||
|   public librariesSearch: string = '' |   public librariesSearch: string = '' | ||||||
| @@ -95,7 +102,35 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|   public sasjsConfig: SASjsConfig = new SASjsConfig() |   public sasjsConfig: SASjsConfig = new SASjsConfig() | ||||||
|   public searchLoading: boolean = false |   public searchLoading: boolean = false | ||||||
|   public searchNumeric: boolean = false |   public searchNumeric: boolean = false | ||||||
|   private hotTableRegisterer: HotTableRegisterer |   @ViewChild(HotTableComponent, { static: false }) | ||||||
|  |   hotTableComponent!: HotTableComponent | ||||||
|  |  | ||||||
|  |   public hotTableSettings: Handsontable.GridSettings = {} | ||||||
|  |  | ||||||
|  |   private updateHotTableSettings(): void { | ||||||
|  |     this.hotTableSettings = { | ||||||
|  |       multiColumnSorting: true, | ||||||
|  |       viewportRowRenderingOffset: 30, | ||||||
|  |       colHeaders: this.hotTable.colHeaders, | ||||||
|  |       columns: this.hotTable.columns, | ||||||
|  |       copyPaste: this.hotTable.copyPaste, | ||||||
|  |       contextMenu: this.hotTable.contextMenu, | ||||||
|  |       filters: true, | ||||||
|  |       dropdownMenu: this.hotTable.dropdownMenu, | ||||||
|  |       height: this.hotTable.height, | ||||||
|  |       stretchH: 'all', | ||||||
|  |       modifyColWidth: this.maxWidthCheker, | ||||||
|  |       cells: this.hotTable.cells, | ||||||
|  |       maxRows: this.hotTable.maxRows, | ||||||
|  |       manualColumnResize: true, | ||||||
|  |       afterGetColHeader: this.hotTable.afterGetColHeader, | ||||||
|  |       rowHeaders: this.hotTable.rowHeaders, | ||||||
|  |       rowHeaderWidth: this.hotTable.rowHeaderWidth, | ||||||
|  |       rowHeights: this.hotTable.rowHeights, | ||||||
|  |       licenseKey: this.hotTable.licenseKey, | ||||||
|  |       className: 'htDark' | ||||||
|  |     } | ||||||
|  |   } | ||||||
|   public numberOfRows: number | null = null |   public numberOfRows: number | null = null | ||||||
|   public headerPks: string[] = [] |   public headerPks: string[] = [] | ||||||
|   public $dataFormats: $DataFormats | null = null |   public $dataFormats: $DataFormats | null = null | ||||||
| @@ -107,11 +142,14 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|   public licenceState = this.licenceService.licenceState |   public licenceState = this.licenceService.licenceState | ||||||
|   public Infinity = Infinity |   public Infinity = Infinity | ||||||
|  |  | ||||||
|  |   private ariaObserver: MutationObserver | undefined | ||||||
|  |   private ariaCheckInterval: any | undefined | ||||||
|  |  | ||||||
|   public hotTable: HotTableInterface = { |   public hotTable: HotTableInterface = { | ||||||
|     data: [], |     data: [], | ||||||
|     colHeaders: [], |     colHeaders: [], | ||||||
|     columns: [], |     columns: [], | ||||||
|     height: '100%', |     height: 'calc(100vh - 182px)', | ||||||
|     maxRows: this.licenceState.value.viewer_rows_allowed || Infinity, |     maxRows: this.licenceState.value.viewer_rows_allowed || Infinity, | ||||||
|     settings: {}, |     settings: {}, | ||||||
|     licenseKey: undefined, |     licenseKey: undefined, | ||||||
| @@ -119,8 +157,30 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|       return ' ' |       return ' ' | ||||||
|     }, |     }, | ||||||
|     afterGetColHeader: (col: number, th: any, headerLevel: number) => { |     afterGetColHeader: (col: number, th: any, headerLevel: number) => { | ||||||
|       // Dark mode |       // CRITICAL: Prevent "colToProp method cannot be called because this Handsontable instance has been destroyed" error | ||||||
|       th.classList.add(globals.handsontable.darkTableHeaderClass) |       // This callback can be triggered even after the instance is destroyed during rapid table switching | ||||||
|  |       if ( | ||||||
|  |         !this.hotInstance || | ||||||
|  |         this.hotInstance.isDestroyed || | ||||||
|  |         this.isTableSwitching | ||||||
|  |       ) { | ||||||
|  |         // Graceful fallback: apply only dark mode styling when instance is unavailable | ||||||
|  |         th.classList.add(globals.handsontable.darkTableHeaderClass) | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         const column = this.hotInstance.colToProp(col) as string | ||||||
|  |  | ||||||
|  |         const isPKCol = column && this.headerPks.indexOf(column) > -1 | ||||||
|  |         if (isPKCol) th.classList.add('primaryKeyHeaderStyle') | ||||||
|  |  | ||||||
|  |         // Apply dark mode styling to all headers | ||||||
|  |         th.classList.add(globals.handsontable.darkTableHeaderClass) | ||||||
|  |       } catch (error) { | ||||||
|  |         // Safety net: if colToProp() fails, still apply basic styling | ||||||
|  |         th.classList.add(globals.handsontable.darkTableHeaderClass) | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|     rowHeaderWidth: 15, |     rowHeaderWidth: 15, | ||||||
|     rowHeights: 20, |     rowHeights: 20, | ||||||
| @@ -154,12 +214,21 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|             let colInfo: DataFormat | undefined |             let colInfo: DataFormat | undefined | ||||||
|             let textInfo = 'No info found' |             let textInfo = 'No info found' | ||||||
|  |  | ||||||
|             if (this.hotInstance) { |             if ( | ||||||
|               const hotSelected: [number, number, number, number][] = |               this.hotInstance && | ||||||
|                 this.hotInstance.getSelected() || [] |               !this.hotInstance.isDestroyed && | ||||||
|               const selectedCol: number = hotSelected ? hotSelected[0][1] : -1 |               !this.isTableSwitching | ||||||
|               const colName = this.hotInstance?.colToProp(selectedCol) |             ) { | ||||||
|               colInfo = this.$dataFormats?.vars[colName] |               try { | ||||||
|  |                 const hotSelected: [number, number, number, number][] = | ||||||
|  |                   this.hotInstance.getSelected() || [] | ||||||
|  |                 const selectedCol: number = hotSelected ? hotSelected[0][1] : -1 | ||||||
|  |                 const colName = this.hotInstance.colToProp(selectedCol) | ||||||
|  |                 colInfo = this.$dataFormats?.vars[colName] | ||||||
|  |               } catch (error) { | ||||||
|  |                 // Ignore errors during table switching | ||||||
|  |                 colInfo = undefined | ||||||
|  |               } | ||||||
|  |  | ||||||
|               if (colInfo) |               if (colInfo) | ||||||
|                 textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}` |                 textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}` | ||||||
| @@ -179,6 +248,13 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|   private hotInstance: Handsontable | null = null |   private hotInstance: Handsontable | null = null | ||||||
|   public hotInstanceClickListener: boolean = false |   public hotInstanceClickListener: boolean = false | ||||||
|  |  | ||||||
|  |   // Race condition prevention for rapid table switching | ||||||
|  |   private isTableSwitching: boolean = false | ||||||
|  |   private switchingTimeout: any = null | ||||||
|  |  | ||||||
|  |   // Prevents duplicate setupHot() calls within short time windows | ||||||
|  |   private lastSetupTime: number = 0 | ||||||
|  |  | ||||||
|   public viewboxOpen: boolean = false |   public viewboxOpen: boolean = false | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
| @@ -193,7 +269,6 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|     private location: Location, |     private location: Location, | ||||||
|     private cdf: ChangeDetectorRef |     private cdf: ChangeDetectorRef | ||||||
|   ) { |   ) { | ||||||
|     this.hotTableRegisterer = new HotTableRegisterer() |  | ||||||
|     this.sasjsConfig = this.sasService.getSasjsConfig() |     this.sasjsConfig = this.sasService.getSasjsConfig() | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -213,6 +288,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|     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 | ||||||
|       } |       } | ||||||
|     ) |     ) | ||||||
|   } |   } | ||||||
| @@ -460,17 +536,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public copyToClip() { |   public copyToClip() { | ||||||
|     let selBox = document.createElement('textarea') |     navigator.clipboard.writeText(this.webQueryText) | ||||||
|     selBox.style.position = 'fixed' |  | ||||||
|     selBox.style.left = '0' |  | ||||||
|     selBox.style.top = '0' |  | ||||||
|     selBox.style.opacity = '0' |  | ||||||
|     selBox.value = this.webQueryText |  | ||||||
|     document.body.appendChild(selBox) |  | ||||||
|     selBox.focus() |  | ||||||
|     selBox.select() |  | ||||||
|     document.execCommand('copy') |  | ||||||
|     document.body.removeChild(selBox) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public goToViewer() { |   public goToViewer() { | ||||||
| @@ -530,7 +596,6 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|         `#search_${library.LIBRARYREF}` |         `#search_${library.LIBRARYREF}` | ||||||
|       ) |       ) | ||||||
|  |  | ||||||
|       this.loggerService.log('[libTreeSearchInput]', libTreeSearchInput) |  | ||||||
|       if (libTreeSearchInput) libTreeSearchInput.focus() |       if (libTreeSearchInput) libTreeSearchInput.focus() | ||||||
|  |  | ||||||
|       if (library && library.libinfo) this.libinfo = library.libinfo |       if (library && library.libinfo) this.libinfo = library.libinfo | ||||||
| @@ -555,10 +620,24 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public onTableClick(libTable: any, library: any) { |   public onTableClick(libTable: any, library: any) { | ||||||
|     this.lib = library.LIBRARYREF |     // OPTIMIZATION: Prevent race conditions and destroyed instance errors during rapid table switching | ||||||
|     this.table = libTable |     if (this.isTableSwitching) { | ||||||
|     this.selectLibTable(libTable) |       return | ||||||
|     this.viewData(0) |     } | ||||||
|  |  | ||||||
|  |     // Clear any existing timeout to prevent stale operations | ||||||
|  |     if (this.switchingTimeout) { | ||||||
|  |       clearTimeout(this.switchingTimeout) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // PERFORMANCE: Debounce table switches to prevent rapid successive calls | ||||||
|  |     // This ensures only the final table selection is processed | ||||||
|  |     this.switchingTimeout = setTimeout(() => { | ||||||
|  |       this.lib = library.LIBRARYREF | ||||||
|  |       this.table = libTable | ||||||
|  |       this.selectLibTable(libTable) | ||||||
|  |       this.viewData(0) | ||||||
|  |     }, 50) // 50ms debounce - fast enough for good UX, slow enough to prevent issues | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async selectTable(lib: string, initial?: boolean, library?: any) { |   public async selectTable(lib: string, initial?: boolean, library?: any) { | ||||||
| @@ -677,6 +756,14 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async viewData(filter_pk: number) { |   public async viewData(filter_pk: number) { | ||||||
|  |     // CRITICAL: Set switching flag to prevent concurrent operations and race conditions | ||||||
|  |     // This prevents callbacks from accessing destroyed instances during table switching | ||||||
|  |     this.isTableSwitching = true | ||||||
|  |  | ||||||
|  |     // CLEANUP: Ensure any existing Handsontable instance is properly destroyed | ||||||
|  |     // This prevents "instance destroyed" errors | ||||||
|  |     this.cleanupHotInstance() | ||||||
|  |  | ||||||
|     this.loadingTableView = true |     this.loadingTableView = true | ||||||
|  |  | ||||||
|     let libDataset: any |     let libDataset: any | ||||||
| @@ -819,27 +906,37 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|           this.versions = res.versions || [] |           this.versions = res.versions || [] | ||||||
|           this.setDSNote() |           this.setDSNote() | ||||||
|           this.queryText = res.sasparams[0].FILTER_TEXT |           this.queryText = res.sasparams[0].FILTER_TEXT | ||||||
|           let columns: any[] = [] |  | ||||||
|           let colArr = [] |  | ||||||
|  |  | ||||||
|           for (let key in res.viewdata[0]) { |           // Initialize columns only if we have data | ||||||
|             if (key) { |           if (res.viewdata && res.viewdata.length > 0) { | ||||||
|               colArr.push(key) |             let columns: any[] = [] | ||||||
|  |             let colArr = [] | ||||||
|  |  | ||||||
|  |             for (let key in res.viewdata[0]) { | ||||||
|  |               if (key) { | ||||||
|  |                 colArr.push(key) | ||||||
|  |               } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             for (let index = 0; index < colArr.length; index++) { | ||||||
|  |               columns.push({ data: colArr[index] }) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             this.hotTable.colHeaders = colArr | ||||||
|  |             this.hotTable.columns = columns | ||||||
|  |           } else { | ||||||
|  |             // Set empty arrays if no data | ||||||
|  |             this.hotTable.colHeaders = [] | ||||||
|  |             this.hotTable.columns = [] | ||||||
|           } |           } | ||||||
|  |  | ||||||
|           for (let index = 0; index < colArr.length; index++) { |           // Set cells function | ||||||
|             columns.push({ data: colArr[index] }) |           this.hotTable.cells = () => { | ||||||
|  |             return { readOnly: true } | ||||||
|           } |           } | ||||||
|  |  | ||||||
|           let cells = function () { |           // Update hot table settings after data is loaded | ||||||
|             let cellProperties = { readOnly: true } |           this.updateHotTableSettings() | ||||||
|             return cellProperties |  | ||||||
|           } |  | ||||||
|  |  | ||||||
|           this.hotTable.colHeaders = colArr |  | ||||||
|           this.hotTable.columns = columns |  | ||||||
|           this.hotTable.cells = cells |  | ||||||
|  |  | ||||||
|           this.tableFlag = false |           this.tableFlag = false | ||||||
|           let ds = [] |           let ds = [] | ||||||
| @@ -907,9 +1004,25 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|  |  | ||||||
|     this.loadingTableView = false |     this.loadingTableView = false | ||||||
|  |  | ||||||
|     //If we try to setup hot when no data is returned it errors `isDestoryed`. |     // Setup Handsontable after async operations complete | ||||||
|     //That is intorduced by HOT update |     // Original issue: setupHot() called before API responses populated headerPks array | ||||||
|     if (!this.noData && !this.noDataReqErr && libDataset) this.setupHot() |     // Solution: Delay ensures both API paths (lines 328 & 886) have chance to set headerPks | ||||||
|  |     setTimeout(() => { | ||||||
|  |       if (!this.noData && !this.noDataReqErr && libDataset) { | ||||||
|  |         this.setupHot() | ||||||
|  |       } | ||||||
|  |     }, 50) // Optimized from 100ms - fast enough for API completion, slow enough to prevent race conditions | ||||||
|  |  | ||||||
|  |     // RACE CONDITION PREVENTION: Reset switching flag after setup completion | ||||||
|  |     // This allows new table switches after current operation finishes | ||||||
|  |     setTimeout(() => { | ||||||
|  |       this.isTableSwitching = false | ||||||
|  |     }, 300) // Optimized from 700ms to match reduced setup times | ||||||
|  |  | ||||||
|  |     // Fix ARIA accessibility issues after data loading | ||||||
|  |     setTimeout(() => { | ||||||
|  |       this.fixAriaAccessibility() | ||||||
|  |     }, 500) | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area. |      * This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area. | ||||||
| @@ -929,7 +1042,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|           }) |           }) | ||||||
|           this.hotInstanceClickListener = true |           this.hotInstanceClickListener = true | ||||||
|         } |         } | ||||||
|       }, 2000) |       }, 1000) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -1057,33 +1170,131 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private setupHot() { |   /** | ||||||
|     setTimeout(() => { |    * CRITICAL CLEANUP (workaround needed for HOT version 16 and above): Safely destroys Handsontable instances | ||||||
|       if (!this.loadingTableView && this.libDataset) { |    * | ||||||
|         this.hotInstance = this.hotTableRegisterer.getInstance('hotInstance') |    * Purpose: Prevents "instance destroyed" errors and memory leaks during table switching | ||||||
|  |    * | ||||||
|  |    * Called from: | ||||||
|  |    * - viewData() - before loading new table data | ||||||
|  |    * - setupHot() - before creating new instance | ||||||
|  |    * - ngOnDestroy() - component cleanup | ||||||
|  |    * | ||||||
|  |    * Safety features: | ||||||
|  |    * - Checks if instance exists and is not already destroyed | ||||||
|  |    * - Try-catch prevents errors during destruction | ||||||
|  |    * - Sets instance to null to prevent stale references | ||||||
|  |    */ | ||||||
|  |   private cleanupHotInstance() { | ||||||
|  |     if (this.hotInstance && !this.hotInstance.isDestroyed) { | ||||||
|  |       try { | ||||||
|  |         this.hotInstance.destroy() | ||||||
|  |       } catch (error) { | ||||||
|  |         console.warn('Error destroying Handsontable instance:', error) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     this.hotInstance = null | ||||||
|  |   } | ||||||
|  |  | ||||||
|         if (this.hotInstance) { |   /** | ||||||
|  |    * PERFORMANCE: Configures Handsontable with enhanced error handling (workaround needed for HOT version 16 and above) | ||||||
|  |    * | ||||||
|  |    * 1. Duplicate call prevention (500ms window) | ||||||
|  |    * 2. Reduced timeout delays (200ms + 50ms vs original 1000ms + 200ms) | ||||||
|  |    * 3. Multiple validation checks to prevent race conditions | ||||||
|  |    * 4. Forced render for immediate primary key styling | ||||||
|  |    * | ||||||
|  |    * Timeline: 50ms (viewData) + 200ms (main) + 50ms (component ready) = ~300ms total | ||||||
|  |    * Previous: 100ms + 600ms + 100ms = 800ms (plus render delays = ~2 seconds) | ||||||
|  |    */ | ||||||
|  |   private setupHot() { | ||||||
|  |     // DUPLICATE PREVENTION: Avoid multiple setup calls during rapid table switching | ||||||
|  |     const now = Date.now() | ||||||
|  |     if (now - this.lastSetupTime < 500) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     this.lastSetupTime = now | ||||||
|  |  | ||||||
|  |     setTimeout(() => { | ||||||
|  |       // VALIDATION: Don't setup if we're currently switching tables or still loading | ||||||
|  |       if (this.loadingTableView || !this.libDataset) { | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // CLEANUP: Ensure clean slate before new setup | ||||||
|  |       this.cleanupHotInstance() | ||||||
|  |  | ||||||
|  |       // TIMING: Wait for Angular component to be ready (optimized from 100ms to 50ms) | ||||||
|  |       setTimeout(() => { | ||||||
|  |         // DOUBLE-CHECK: Ensure we're still in valid state after delays | ||||||
|  |         if ( | ||||||
|  |           this.isTableSwitching || | ||||||
|  |           this.loadingTableView || | ||||||
|  |           !this.libDataset | ||||||
|  |         ) { | ||||||
|  |           return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.hotInstance = this.hotTableComponent?.hotInstance | ||||||
|  |  | ||||||
|  |         if (this.hotInstance && !this.hotInstance.isDestroyed) { | ||||||
|           this.hotInstance.updateSettings({ |           this.hotInstance.updateSettings({ | ||||||
|             height: this.hotTable.height, |             height: this.hotTable.height, | ||||||
|             modifyColWidth: function (width: any, col: any) { |             modifyColWidth: (width: any, col: any) => { | ||||||
|               if (width > 500) return 500 |               if (width > 500) return 500 | ||||||
|               else return width |               else return width | ||||||
|             }, |             }, | ||||||
|             afterGetColHeader: (col: number, th: any) => { |             afterGetColHeader: (col: number, th: any) => { | ||||||
|               const column = this.hotInstance?.colToProp(col) as string |               // CRITICAL: Same safety checks as initial callback to prevent destroyed instance errors | ||||||
|  |               if ( | ||||||
|  |                 !this.hotInstance || | ||||||
|  |                 this.hotInstance.isDestroyed || | ||||||
|  |                 this.isTableSwitching | ||||||
|  |               ) { | ||||||
|  |                 th.classList.add(globals.handsontable.darkTableHeaderClass) | ||||||
|  |                 return | ||||||
|  |               } | ||||||
|  |  | ||||||
|               // header columns styling - primary keys |               try { | ||||||
|               const isPKCol = column && this.headerPks.indexOf(column) > -1 |                 const column = this.hotInstance.colToProp(col) as string | ||||||
|  |  | ||||||
|               if (isPKCol) th.classList.add('primaryKeyHeaderStyle') |                 // PRIMARY KEY STYLING: Apply special styling to PK columns (populated from API response) | ||||||
|  |                 const isPKCol = column && this.headerPks.indexOf(column) > -1 | ||||||
|  |                 if (isPKCol) th.classList.add('primaryKeyHeaderStyle') | ||||||
|  |  | ||||||
|               // Dark mode |                 // DARK MODE: Apply to all headers | ||||||
|               th.classList.add(globals.handsontable.darkTableHeaderClass) |                 th.classList.add(globals.handsontable.darkTableHeaderClass) | ||||||
|  |               } catch (error) { | ||||||
|  |                 // SAFETY NET: Ensure basic styling is always applied | ||||||
|  |                 th.classList.add(globals.handsontable.darkTableHeaderClass) | ||||||
|  |               } | ||||||
|             } |             } | ||||||
|           }) |           }) | ||||||
|  |  | ||||||
|  |           // Add hooks for accessibility fixes | ||||||
|  |           this.hotInstance.addHook('afterRender', () => { | ||||||
|  |             // Fix ARIA accessibility issues after each render | ||||||
|  |             this.fixAriaAccessibility() | ||||||
|  |           }) | ||||||
|  |  | ||||||
|  |           this.hotInstance.addHook('afterChange', () => { | ||||||
|  |             // Fix ARIA accessibility issues after any data change | ||||||
|  |             setTimeout(() => { | ||||||
|  |               this.fixAriaAccessibility() | ||||||
|  |             }, 50) | ||||||
|  |           }) | ||||||
|  |  | ||||||
|  |           // Force immediate render to apply primary key styling | ||||||
|  |           // Without this, styling would wait for ~2 seconds to be applied | ||||||
|  |           // With this, styling appears in ~300ms total (workaround needed for HOT version 16 and above) | ||||||
|  |           setTimeout(() => { | ||||||
|  |             if (this.hotInstance && !this.hotInstance.isDestroyed) { | ||||||
|  |               this.hotInstance.render() | ||||||
|  |             } | ||||||
|  |           }, 10) | ||||||
|         } |         } | ||||||
|       } |       }, 50) // Optimized Angular component readiness delay | ||||||
|     }, 1000) |     }, 200) // Optimized main setup delay (was 600ms) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async loadWithParameters() { |   async loadWithParameters() { | ||||||
| @@ -1148,7 +1359,187 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   ngAfterViewInit() {} |   ngAfterViewInit() { | ||||||
|  |     // Fix ARIA accessibility issues after table initialization | ||||||
|  |     setTimeout(() => { | ||||||
|  |       this.fixAriaAccessibility() | ||||||
|  |     }, 1000) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ngOnDestroy() { | ||||||
|  |     // Proper component destruction to prevent memory leaks and errors | ||||||
|  |  | ||||||
|  |     // Prevent any new operations during cleanup | ||||||
|  |     this.isTableSwitching = true | ||||||
|  |  | ||||||
|  |     // Clear any pending debounced table switches | ||||||
|  |     if (this.switchingTimeout) { | ||||||
|  |       clearTimeout(this.switchingTimeout) | ||||||
|  |       this.switchingTimeout = null | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Safely destroy Handsontable instance | ||||||
|  |     this.cleanupHotInstance() | ||||||
|  |  | ||||||
|  |     // Clean up ARIA accessibility observers | ||||||
|  |     if (this.ariaObserver) { | ||||||
|  |       this.ariaObserver.disconnect() | ||||||
|  |       this.ariaObserver = undefined | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Clear ARIA check intervals | ||||||
|  |     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' | ||||||
|  |         ] | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   async ngAfterContentInit() { |   async ngAfterContentInit() { | ||||||
|     if (this.hotTable.data.length > 0) { |     if (this.hotTable.data.length > 0) { | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core' | |||||||
| import { CommonModule } from '@angular/common' | import { CommonModule } from '@angular/common' | ||||||
| import { ViewerComponent } from './viewer.component' | import { ViewerComponent } from './viewer.component' | ||||||
| import { ViewRouteComponent } from '../routes/view-route/view-route.component' | import { ViewRouteComponent } from '../routes/view-route/view-route.component' | ||||||
| import { HotTableModule } from '@handsontable/angular' | import { HotTableModule } from '@handsontable/angular-wrapper' | ||||||
| import { ViewerRoutingModule } from './viewer-routing.module' | import { ViewerRoutingModule } from './viewer-routing.module' | ||||||
| import { ClarityModule } from '@clr/angular' | import { ClarityModule } from '@clr/angular' | ||||||
| import { FormsModule } from '@angular/forms' | import { FormsModule } from '@angular/forms' | ||||||
| @@ -36,7 +36,7 @@ import { MetadataComponent } from '../metadata/metadata.component' | |||||||
|     ClipboardModule, |     ClipboardModule, | ||||||
|     FormsModule, |     FormsModule, | ||||||
|     ClarityModule, |     ClarityModule, | ||||||
|     HotTableModule.forRoot(), |     HotTableModule, | ||||||
|     AppSharedModule, |     AppSharedModule, | ||||||
|     SharedModule, |     SharedModule, | ||||||
|     PipesModule, |     PipesModule, | ||||||
|   | |||||||
| @@ -125,30 +125,9 @@ | |||||||
|  |  | ||||||
|     <div class="clr-flex-1"> |     <div class="clr-flex-1"> | ||||||
|       <hot-table |       <hot-table | ||||||
|         hotId="hotInstance" |  | ||||||
|         id="hot-table" |         id="hot-table" | ||||||
|         className="htDark" |  | ||||||
|         [multiColumnSorting]="true" |  | ||||||
|         [viewportRowRenderingOffset]="50" |  | ||||||
|         [data]="selectedTab === TabsEnum.Rules ? xlmapRules : xlData" |         [data]="selectedTab === TabsEnum.Rules ? xlmapRules : xlData" | ||||||
|         [colHeaders]=" |         [settings]="hotTableSettings" | ||||||
|           selectedTab === TabsEnum.Rules ? xlmapRulesHeaders : xlUploadHeader |  | ||||||
|         " |  | ||||||
|         [columns]=" |  | ||||||
|           selectedTab === TabsEnum.Rules ? xlmapRulesColumns : xlUploadColumns |  | ||||||
|         " |  | ||||||
|         [filters]="true" |  | ||||||
|         [height]="'100%'" |  | ||||||
|         stretchH="all" |  | ||||||
|         [afterGetColHeader]="afterGetColHeader" |  | ||||||
|         [modifyColWidth]="maxWidthChecker" |  | ||||||
|         [cells]="getCellConfiguration" |  | ||||||
|         [maxRows]="hotTableMaxRows" |  | ||||||
|         [manualColumnResize]="true" |  | ||||||
|         [rowHeaders]="rowHeaders" |  | ||||||
|         [rowHeaderWidth]="15" |  | ||||||
|         [rowHeights]="20" |  | ||||||
|         [licenseKey]="hotTableLicenseKey" |  | ||||||
|       > |       > | ||||||
|       </hot-table> |       </hot-table> | ||||||
|     </div> |     </div> | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ import { | |||||||
| } from '../services' | } from '../services' | ||||||
| import { getCellAddress, getFinishingCell } from './utils/xl.utils' | import { getCellAddress, getFinishingCell } from './utils/xl.utils' | ||||||
| import { blobToFile, byteArrayToBinaryString } from './utils/file.utils' | import { blobToFile, byteArrayToBinaryString } from './utils/file.utils' | ||||||
|  | import Handsontable from 'handsontable' | ||||||
| import { UploadFileResponse } from '../models/UploadFile' | import { UploadFileResponse } from '../models/UploadFile' | ||||||
|  |  | ||||||
| interface XLMapRule { | interface XLMapRule { | ||||||
| @@ -136,6 +137,34 @@ export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit { | |||||||
|   public hotTableMaxRows = |   public hotTableMaxRows = | ||||||
|     this.licenceState.value.viewer_rows_allowed || Infinity |     this.licenceState.value.viewer_rows_allowed || Infinity | ||||||
|  |  | ||||||
|  |   get hotTableSettings(): Handsontable.GridSettings { | ||||||
|  |     return { | ||||||
|  |       multiColumnSorting: true, | ||||||
|  |       viewportRowRenderingOffset: 50, | ||||||
|  |       colHeaders: | ||||||
|  |         this.selectedTab === this.TabsEnum.Rules | ||||||
|  |           ? this.xlmapRulesHeaders | ||||||
|  |           : this.xlUploadHeader, | ||||||
|  |       columns: | ||||||
|  |         this.selectedTab === this.TabsEnum.Rules | ||||||
|  |           ? this.xlmapRulesColumns | ||||||
|  |           : this.xlUploadColumns, | ||||||
|  |       filters: true, | ||||||
|  |       height: '100%', | ||||||
|  |       stretchH: 'all', | ||||||
|  |       afterGetColHeader: this.afterGetColHeader, | ||||||
|  |       modifyColWidth: this.maxWidthChecker, | ||||||
|  |       cells: this.getCellConfiguration, | ||||||
|  |       maxRows: this.hotTableMaxRows, | ||||||
|  |       manualColumnResize: true, | ||||||
|  |       rowHeaders: this.rowHeaders, | ||||||
|  |       rowHeaderWidth: 15, | ||||||
|  |       rowHeights: 20, | ||||||
|  |       licenseKey: this.hotTableLicenseKey, | ||||||
|  |       className: 'htDark' | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     private eventService: EventService, |     private eventService: EventService, | ||||||
|     private licenceService: LicenceService, |     private licenceService: LicenceService, | ||||||
|   | |||||||
| @@ -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' | ||||||
|   | |||||||
| @@ -45,7 +45,7 @@ | |||||||
|  |  | ||||||
|   <sasjs |   <sasjs | ||||||
|     serverUrl="" |     serverUrl="" | ||||||
|     appLoc="/Public/app/dc" |     appLoc="/Public/app/devtest" | ||||||
|     serverType="SASJS" |     serverType="SASJS" | ||||||
|     loginMechanism="Redirected" |     loginMechanism="Redirected" | ||||||
|     debug="false" |     debug="false" | ||||||
|   | |||||||
| @@ -937,11 +937,6 @@ app-multi-dataset { | |||||||
|   .dataset-input-wrapper { |   .dataset-input-wrapper { | ||||||
|     max-width: 500px; |     max-width: 500px; | ||||||
|     width: 100%; |     width: 100%; | ||||||
|  |  | ||||||
|     textarea { |  | ||||||
|       min-height: 200px; |  | ||||||
|       height: 200px; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .submit-reason { |   .submit-reason { | ||||||
| @@ -1042,12 +1037,12 @@ app-approve { | |||||||
| // HISTORY.COMPONENT | // HISTORY.COMPONENT | ||||||
| app-history { | app-history { | ||||||
|   .rejected { |   .rejected { | ||||||
|     color: #f83126; |     color: #92201a; | ||||||
|     font-weight: bold |     font-weight: bold | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .accepted { |   .accepted { | ||||||
|     color: #3fc424; |     color: #105c26; | ||||||
|     font-weight: bold |     font-weight: bold | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -1206,6 +1201,8 @@ app-viewer { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   hot-table { |   hot-table { | ||||||
|  |     height: calc(100vh - 200px); | ||||||
|  |  | ||||||
|     .handsontable tbody th.ht__highlight, .handsontable thead th.ht__highlight { |     .handsontable tbody th.ht__highlight, .handsontable thead th.ht__highlight { | ||||||
|       &.primaryKeyHeaderStyle { |       &.primaryKeyHeaderStyle { | ||||||
|         background-color: #306b00b0 !important; |         background-color: #306b00b0 !important; | ||||||
| @@ -3442,6 +3439,10 @@ app-approve-details { | |||||||
|     width: 175px |     width: 175px | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   #rejectBtn { | ||||||
|  |     background-color: #a62f16 !important; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   .formatted-values-toggle { |   .formatted-values-toggle { | ||||||
|     min-width: 75px |     min-width: 75px | ||||||
|   } |   } | ||||||
| @@ -3630,12 +3631,12 @@ app-excel-password-modal { | |||||||
| // STAGE.COMPONENT | // STAGE.COMPONENT | ||||||
| app-stage { | app-stage { | ||||||
|   .rejected { |   .rejected { | ||||||
|     color: #f83126; |     color: #92201a; | ||||||
|     font-weight: bold |     font-weight: bold | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .accepted { |   .accepted { | ||||||
|     color: #3fc424; |     color: #105c26; | ||||||
|     font-weight: bold |     font-weight: bold | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -3644,6 +3645,25 @@ app-stage { | |||||||
|     margin-top:10px; |     margin-top:10px; | ||||||
|     color: #007cbb; |     color: #007cbb; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // Accessibility fixes for handsontable focus catchers | ||||||
|  |   .htFocusCatcher { | ||||||
|  |     // Hide from screen readers but maintain functionality | ||||||
|  |     position: absolute !important; | ||||||
|  |     left: -9999px !important; | ||||||
|  |     width: 1px !important; | ||||||
|  |     height: 1px !important; | ||||||
|  |     overflow: hidden !important; | ||||||
|  |     clip: rect(0, 0, 0, 0) !important; | ||||||
|  |     border: 0 !important; | ||||||
|  |     margin: -1px !important; | ||||||
|  |     padding: 0 !important; | ||||||
|  |  | ||||||
|  |     // Ensure it's not focusable by screen readers | ||||||
|  |     &:focus { | ||||||
|  |       outline: none !important; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| body[cds-theme="dark"] { | body[cds-theme="dark"] { | ||||||
| @@ -4746,7 +4766,6 @@ body[cds-theme="dark"] { | |||||||
| } | } | ||||||
|  |  | ||||||
| .handsontable.listbox { | .handsontable.listbox { | ||||||
|   padding: 5px 0px 5px 5px; |  | ||||||
|   box-shadow: 0px 4px 20px 0px #00000070; |   box-shadow: 0px 4px 20px 0px #00000070; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -4756,6 +4775,12 @@ body[cds-theme="dark"] { | |||||||
|   color: #ffffff !important; |   color: #ffffff !important; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .handsontable td.dc-invalid-cell { | ||||||
|  |   background: #e62700ad !important; | ||||||
|  |   border: 1px solid red !important; | ||||||
|  |   color: #ffffff !important; | ||||||
|  | } | ||||||
|  |  | ||||||
| .handsontable .numericListbox { | .handsontable .numericListbox { | ||||||
|   text-align: right; |   text-align: right; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,12 +1,12 @@ | |||||||
| { | { | ||||||
|   "name": "dcfrontend", |   "name": "dcfrontend", | ||||||
|   "version": "6.14.7", |   "version": "7.1.1", | ||||||
|   "lockfileVersion": 3, |   "lockfileVersion": 3, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "packages": { |   "packages": { | ||||||
|     "": { |     "": { | ||||||
|       "name": "dcfrontend", |       "name": "dcfrontend", | ||||||
|       "version": "6.14.7", |       "version": "7.1.1", | ||||||
|       "hasInstallScript": true, |       "hasInstallScript": true, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "@saithodev/semantic-release-gitea": "^2.1.0", |         "@saithodev/semantic-release-gitea": "^2.1.0", | ||||||
| @@ -16,7 +16,7 @@ | |||||||
|         "@semantic-release/npm": "11.0.0", |         "@semantic-release/npm": "11.0.0", | ||||||
|         "@semantic-release/release-notes-generator": "^11.0.4", |         "@semantic-release/release-notes-generator": "^11.0.4", | ||||||
|         "commit-and-tag-version": "^11.2.2", |         "commit-and-tag-version": "^11.2.2", | ||||||
|         "prettier": "3.2.5" |         "prettier": "3.6.2" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/code-frame": { |     "node_modules/@babel/code-frame": { | ||||||
| @@ -6690,9 +6690,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/prettier": { |     "node_modules/prettier": { | ||||||
|       "version": "3.2.5", |       "version": "3.6.2", | ||||||
|       "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", |       "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", | ||||||
|       "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", |       "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "bin": { |       "bin": { | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,25 +1,26 @@ | |||||||
| { | { | ||||||
|   "name": "dcfrontend", |   "name": "dcfrontend", | ||||||
|   "version": "7.0.0", |   "version": "7.2.4", | ||||||
|   "description": "Data Controller", |   "description": "Data Controller", | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@saithodev/semantic-release-gitea": "^2.1.0", |     "@saithodev/semantic-release-gitea": "^2.1.0", | ||||||
|     "@semantic-release/changelog": "^6.0.3", |     "@semantic-release/changelog": "^6.0.3", | ||||||
|     "@semantic-release/commit-analyzer": "^10.0.1", |     "@semantic-release/commit-analyzer": "^10.0.1", | ||||||
|     "@semantic-release/npm": "11.0.0", |  | ||||||
|     "@semantic-release/git": "^10.0.1", |     "@semantic-release/git": "^10.0.1", | ||||||
|  |     "@semantic-release/npm": "11.0.0", | ||||||
|     "@semantic-release/release-notes-generator": "^11.0.4", |     "@semantic-release/release-notes-generator": "^11.0.4", | ||||||
|     "commit-and-tag-version": "^11.2.2", |     "commit-and-tag-version": "^11.2.2", | ||||||
|     "prettier": "3.2.5" |     "prettier": "3.6.2" | ||||||
|   }, |   }, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "install": "cd client && npm i && cd ../sas && npm i", |     "install": "cd client && npm i && cd ../sas && npm i", | ||||||
|     "build-frontend": "cd client && npm run build", |     "build-frontend": "cd client && npm run build", | ||||||
|     "release": "commit-and-tag-version", |     "release": "commit-and-tag-version", | ||||||
|     "lint": "npm run lint:fix", |     "lint": "npm run lint:fix", | ||||||
|     "lint:fix": "npx prettier --write \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/integration/*.tests.ts\"", |     "lint:fix": "npx prettier --write \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/e2e/*.cy.ts\"", | ||||||
|     "lint:check": "npx prettier --check \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/integration/*.tests.ts\"", |     "lint:fix:silent": "npx prettier --log-level silent --write \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/e2e/*.cy.ts\"", | ||||||
|     "lint:silent": "npx prettier --loglevel silent --check \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/integration/*.tests.ts\"", |     "lint:check": "npx prettier --check \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/e2e/*.cy.ts\"", | ||||||
|  |     "lint:check:silent": "npx prettier --log-level silent --check \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/e2e/*.cy.ts\"", | ||||||
|     "jo": "echo", |     "jo": "echo", | ||||||
|     "prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true" |     "prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true" | ||||||
|   }, |   }, | ||||||
| @@ -31,5 +32,6 @@ | |||||||
|   "//": [ |   "//": [ | ||||||
|     "Readme", |     "Readme", | ||||||
|     "We must set private: true so that semantic-release/npm plugin will update the package.json version but not try to release it as NPM package" |     "We must set private: true so that semantic-release/npm plugin will update the package.json version but not try to release it as NPM package" | ||||||
|   ] |   ], | ||||||
|  |   "dependencies": {} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,6 +13,12 @@ if (fs.existsSync(sessionStoragePath)){ | |||||||
|     } catch (err) {} |     } catch (err) {} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | let controlTableText = '' | ||||||
|  |  | ||||||
|  | if (_WEBIN_FILENAME1.includes('SASControlTable')) controlTableText = _WEBIN_FILEREF1.toString() | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| let webouts = { | let webouts = { | ||||||
|     MPE_X_TEST: `{"SYSDATE" : "26SEP22" |     MPE_X_TEST: `{"SYSDATE" : "26SEP22" | ||||||
|     ,"SYSTIME" : "08:48" |     ,"SYSTIME" : "08:48" | ||||||
|   | |||||||
							
								
								
									
										699
									
								
								sas/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										699
									
								
								sas/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -28,7 +28,7 @@ | |||||||
|   }, |   }, | ||||||
|   "private": true, |   "private": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@sasjs/cli": "^4.12.8", |     "@sasjs/cli": "^4.12.11", | ||||||
|     "@sasjs/core": "^4.59.1" |     "@sasjs/core": "^4.59.5" | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -39,15 +39,35 @@ | |||||||
|   %let &outresult=NO; |   %let &outresult=NO; | ||||||
|   %let &outreason=NOTFOUND; |   %let &outreason=NOTFOUND; | ||||||
|  |  | ||||||
|   /* check if there is actually a version to restore */ |   %local libds; | ||||||
|  |   proc sql noprint; | ||||||
|  |   select upcase(cats(base_lib,'.',base_ds)) into: libds | ||||||
|  |     from &dc_libref..mpe_submit | ||||||
|  |     where TABLE_ID="&load_ref"; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |     * check if there is actually a version to restore | ||||||
|  |     */ | ||||||
|  |   %local audtab; | ||||||
|  |   proc sql noprint; | ||||||
|  |   select coalescec(audit_libds,"&dc_libref..MPE_AUDIT") into: audtab | ||||||
|  |   from &dclib..MPE_TABLES | ||||||
|  |   where &dc_dttmtfmt. lt tx_to | ||||||
|  |     and libref="%scan(&libds,1,.)" and dsn="%scan(&libds,2,.)"; | ||||||
|  |   %if "&audtab"="0" %then %do; | ||||||
|  |     %let allow_restore=NO; | ||||||
|  |     %let reason= &libds has no audit table configured; | ||||||
|  |     %return; | ||||||
|  |   %end; | ||||||
|  |  | ||||||
|   %local chk; |   %local chk; | ||||||
|   %let chk=0; |   %let chk=0; | ||||||
|   proc sql noprint; |   proc sql noprint; | ||||||
|   select count(*) into: chk from &dc_libref..mpe_audit |   select count(*) into: chk from &audtab | ||||||
|     where load_ref="&load_ref"; |     where load_ref="&load_ref"; | ||||||
|   %if &chk=0 %then %do; |   %if &chk=0 %then %do; | ||||||
|     %let allow_restore=NO; |     %let allow_restore=NO; | ||||||
|     %let reason=No entry for &load_ref in MPE_AUDIT; |     %let reason=No entry for &load_ref in &audtab; | ||||||
|     %return; |     %return; | ||||||
|   %end; |   %end; | ||||||
|  |  | ||||||
| @@ -70,11 +90,6 @@ | |||||||
|   %end; |   %end; | ||||||
|  |  | ||||||
|   /* check if user has basic access */ |   /* check if user has basic access */ | ||||||
|   %local libds; |  | ||||||
|   proc sql noprint; |  | ||||||
|   select cats(base_lib,'.',base_ds) into: libds |  | ||||||
|     from &mpelib..mpe_submit |  | ||||||
|     where TABLE_ID="&load_ref"; |  | ||||||
|   %mpe_accesscheck(&libds,outds=work.access_check |   %mpe_accesscheck(&libds,outds=work.access_check | ||||||
|     ,user=&user |     ,user=&user | ||||||
|     ,access_level=EDIT |     ,access_level=EDIT | ||||||
|   | |||||||
| @@ -326,14 +326,20 @@ proc sql; | |||||||
|       ,count(*) as table_cnt |       ,count(*) as table_cnt | ||||||
|     from statustabs |     from statustabs | ||||||
|     group by 1; |     group by 1; | ||||||
|  |   create table work.libs as | ||||||
|  |     select libref from work.sumcat | ||||||
|  |     union | ||||||
|  |     select libref from work.sumdsn; | ||||||
|   create table work.statuslibs as |   create table work.statuslibs as | ||||||
|     select coalesce(a.libref,b.libref) as libref, |     select a.libref, | ||||||
|       a.libsize, |       b.libsize, | ||||||
|       a.table_cnt, |       b.table_cnt, | ||||||
|       b.catalog_cnt |       c.catalog_cnt | ||||||
|     from work.sumdsn a |     from work.libs a | ||||||
|     full join work.sumcat b |     left join work.sumdsn b | ||||||
|     on a.libref=b.libref; |     on a.libref=b.libref | ||||||
|  |     left join work.sumcat c | ||||||
|  |     on a.libref=c.libref; | ||||||
|  |  | ||||||
|   %bitemporal_dataloader(base_lib=&mpelib |   %bitemporal_dataloader(base_lib=&mpelib | ||||||
|     ,base_dsn=mpe_datastatus_libs |     ,base_dsn=mpe_datastatus_libs | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user