Compare commits
	
		
			177 Commits
		
	
	
		
			v6.2.8
			...
			d77f2eb674
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					d77f2eb674 | ||
| 
						 | 
					5474fad9cc | ||
| 
						 | 
					3dd85cc60b | ||
| 
						 | 
					510e412ff2 | ||
| 
						 | 
					3dfdccbc6b | ||
| 
						 | 
					a55661548a | ||
| 
						 | 
					69363b37e9 | ||
| 
						 | 
					2d6a753921 | ||
| 
						 | 
					dc989e5668 | ||
| 
						 | 
					227ac480d5 | ||
| 
						 | 
					c5e4650327 | ||
| 56cf271e77 | |||
| 
						 | 
					fa8396f039 | ||
| 
						 | 
					904ca30f91 | ||
| 
						 | 
					549f35766b | ||
| 
						 | 
					1589c799ec | ||
| 
						 | 
					604c2e70bd | ||
| 
						 | 
					297a84d3a4 | ||
| 
						 | 
					aaad9f7207 | ||
| 
						 | 
					a4028562ce | ||
| 
						 | 
					5ab3f98855 | ||
| 
						 | 
					eba21e96b4 | ||
| 
						 | 
					9ad7ae47b5 | ||
| cf54e4c8f3 | |||
| 
						 | 
					57aa6fa0fc | ||
| 
						 | 
					4a01f3d490 | ||
| 
						 | 
					80a0db951d | ||
| 
						 | 
					3202cb8e08 | ||
| 
						 | 
					ddf36230bf | ||
| 
						 | 
					b67c2be968 | ||
| 
						 | 
					dc2c8da92b | ||
| 
						 | 
					5d6c3701d0 | ||
| 
						 | 
					b024e263b4 | ||
| b706864e40 | |||
| 
						 | 
					60cc666b67 | ||
| 
						 | 
					efff4dd553 | ||
| 
						 | 
					2b1dad8e48 | ||
| 
						 | 
					8c7de5aad7 | ||
| 
						 | 
					1db8bc2573 | ||
| 
						 | 
					c60dd65a16 | ||
| f7f59a4b0a | |||
| 
						 | 
					ec7615e7e3 | ||
| 
						 | 
					f411c33754 | ||
| 
						 | 
					79121168e4 | ||
| 
						 | 
					ef81e33f70 | ||
| 
						 | 
					96066c66cb | ||
| 
						 | 
					b1819b776d | ||
| 
						 | 
					bd7a392ffc | ||
| 7997b77158 | |||
| 
						 | 
					d1966bcdc5 | ||
| 
						 | 
					7b5bbe024d | ||
| 4d84f15aca | |||
| 
						 | 
					928937daab | ||
| 
						 | 
					3bd8d247e5 | ||
| 
						 | 
					cf6c9dd5f2 | ||
| 
						 | 
					ff55cbbaad | ||
| 
						 | 
					3eda4e2c58 | ||
| 
						 | 
					02a8a1c565 | ||
| 
						 | 
					8769841f08 | ||
| 
						 | 
					7208fe1c3b | ||
| 
						 | 
					801c8c6a9f | ||
| 
						 | 
					51ebd25aa3 | ||
| 
						 | 
					c6595c1f61 | ||
| 
						 | 
					a267666e99 | ||
| 
						 | 
					b27fea5b91 | ||
| 
						 | 
					f8a14d4bde | ||
| 
						 | 
					633e35338d | ||
| 
						 | 
					8003da94e6 | ||
| c3af97ef57 | |||
| 31d4e5c727 | |||
| fbbcf90956 | |||
| f522038b8d | |||
| 
						 | 
					ace599b39f | ||
| 
						 | 
					963562621d | ||
| 
						 | 
					5171d07441 | ||
| 9a0b9573d5 | |||
| 
						 | 
					4733311ef3 | ||
| 
						 | 
					432450a15b | ||
| 
						 | 
					47638becc0 | ||
| bdd3a95685 | |||
| 
						 | 
					38601346a5 | ||
| 
						 | 
					dc3a6ae6a1 | ||
| f668b1e7f7 | |||
| eb1c09d790 | |||
| 
						 | 
					9bf324c74b | ||
| f13e909478 | |||
| 
						 | 
					6a0fe287dd | ||
| 
						 | 
					5a48f2e6e3 | ||
| 
						 | 
					6565834ad4 | ||
| 
						 | 
					837821fd01 | ||
| 
						 | 
					cff5989559 | ||
| 
						 | 
					60510a4d68 | ||
| 
						 | 
					2b54034973 | ||
| 
						 | 
					347b0f9065 | ||
| 
						 | 
					eac0104d7a | ||
| 
						 | 
					1c8e4604de | ||
| e9624635ed | |||
| 
						 | 
					f9beda1ddb | ||
| 
						 | 
					53400de110 | ||
| cf37ddab22 | |||
| 
						 | 
					625af199f4 | ||
| 56e9217f4b | |||
| 86f1af7926 | |||
| 
						 | 
					7737f8455d | ||
| b0f1677fcc | |||
| 
						 | 
					4406e0d4b4 | ||
| cf19381060 | |||
| 802d8a3b08 | |||
| 
						 | 
					2a852496e9 | ||
| 
						 | 
					4653097225 | ||
| 
						 | 
					8afee29e02 | ||
| 
						 | 
					233eca39ef | ||
| 
						 | 
					1a96bb1233 | ||
| 
						 | 
					93702c63dc | ||
| 
						 | 
					df065562d1 | ||
| 
						 | 
					802c99adf9 | ||
| 
						 | 
					482c7455f5 | ||
| 
						 | 
					731b96dccc | ||
| 
						 | 
					9550ae4d11 | ||
| 
						 | 
					2d6e747db9 | ||
| 
						 | 
					fd94945466 | ||
| 
						 | 
					d3b0c09332 | ||
| 
						 | 
					01915a2db9 | ||
| 
						 | 
					51b043b6d2 | ||
| 
						 | 
					c144fd8087 | ||
| 
						 | 
					12b15df78c | ||
| 
						 | 
					d6ecd12cea | ||
| 
						 | 
					1c3d498da6 | ||
| 
						 | 
					d75e10aef5 | ||
| 
						 | 
					f0f9d85558 | ||
| 86f3411896 | |||
| 
						 | 
					6daef39268 | ||
| 7d1720a360 | |||
| 
						 | 
					b11a4884b4 | ||
| 
						 | 
					50696bb926 | ||
| 
						 | 
					d67d4e2f86 | ||
| 
						 | 
					2f01c4d251 | ||
| 
						 | 
					9ffa30ab74 | ||
| 
						 | 
					5d93346b52 | ||
| 
						 | 
					39762b36c6 | ||
| e40ebdff05 | |||
| 
						 | 
					8d12d9e51e | ||
| 
						 | 
					23708c9aae | ||
| 
						 | 
					c86fba9dc7 | ||
| 
						 | 
					e747e6e4e7 | ||
| 
						 | 
					5aec024242 | ||
| 
						 | 
					b473b198a6 | ||
| 
						 | 
					516e5a2062 | ||
| 
						 | 
					fb3abbe491 | ||
| 
						 | 
					3e009f3037 | ||
| 
						 | 
					e63d304953 | ||
| 3cd90c2d47 | |||
| 
						 | 
					a485c3b787 | ||
| 
						 | 
					2702bb3c84 | ||
| 
						 | 
					56264ecc69 | ||
| 
						 | 
					cc4535245c | ||
| 
						 | 
					6ae31de1dd | ||
| 
						 | 
					2d4d068413 | ||
| 
						 | 
					271543a446 | ||
| 
						 | 
					8f796aec36 | ||
| 
						 | 
					6eb1aa85d2 | ||
| ac59b77ad5 | |||
| 3efccc4cf3 | |||
| 
						 | 
					8cbcd18f4b | ||
| 6bb2378790 | |||
| e7d0ffe8c0 | |||
| 
						 | 
					ab89600c73 | ||
| 
						 | 
					830e3816a0 | ||
| 
						 | 
					dadac4f13f | ||
| 
						 | 
					1de48a49af | ||
| 
						 | 
					687a1e1cb5 | ||
| 
						 | 
					665a04f5c5 | ||
| 
						 | 
					fdb18d242b | ||
| 
						 | 
					ec173da4ce | ||
| 
						 | 
					bb35cc15d2 | ||
| 
						 | 
					181f52eaea | ||
| 
						 | 
					fc7c8101ed | 
@@ -10,7 +10,7 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 18
 | 
			
		||||
          node-version: 20
 | 
			
		||||
 | 
			
		||||
      - name: Write .npmrc file
 | 
			
		||||
        run: echo "$NPMRC" > client/.npmrc
 | 
			
		||||
@@ -18,8 +18,24 @@ jobs:
 | 
			
		||||
        env:
 | 
			
		||||
          NPMRC: ${{ secrets.NPMRC}}
 | 
			
		||||
 | 
			
		||||
      - run: npm run lint:check
 | 
			
		||||
      - run: |
 | 
			
		||||
      - name: Lint check
 | 
			
		||||
        run: npm run lint:check
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: |
 | 
			
		||||
          cd client
 | 
			
		||||
          npm ci
 | 
			
		||||
          npm run license-checker
 | 
			
		||||
          # Decrypt and Install sheet
 | 
			
		||||
          echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
 | 
			
		||||
          npm i ./libraries/sheet-crypto.tgz
 | 
			
		||||
          # End
 | 
			
		||||
 | 
			
		||||
      - name: Licence checker
 | 
			
		||||
        run: |
 | 
			
		||||
          cd client
 | 
			
		||||
          npm run license-checker
 | 
			
		||||
 | 
			
		||||
      - name: Angular Tests
 | 
			
		||||
        run: |
 | 
			
		||||
          cd client
 | 
			
		||||
          npm run test:headless
 | 
			
		||||
@@ -13,7 +13,7 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 18
 | 
			
		||||
          node-version: 20
 | 
			
		||||
 | 
			
		||||
      - name: Write .npmrc file
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -34,7 +34,13 @@ jobs:
 | 
			
		||||
          CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: npm ci
 | 
			
		||||
        run: |
 | 
			
		||||
          cd client
 | 
			
		||||
          npm ci
 | 
			
		||||
          # Decrypt and Install sheet
 | 
			
		||||
          echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
 | 
			
		||||
          npm i ./libraries/sheet-crypto.tgz
 | 
			
		||||
          # End
 | 
			
		||||
 | 
			
		||||
      - name: Check audit
 | 
			
		||||
      # Audit should fail and stop the CI if critical vulnerability found
 | 
			
		||||
@@ -48,7 +54,7 @@ jobs:
 | 
			
		||||
      - name: Angular Tests
 | 
			
		||||
        run: |
 | 
			
		||||
          cd client
 | 
			
		||||
          npm test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
 | 
			
		||||
          npm run test:headless
 | 
			
		||||
 | 
			
		||||
      - name: Angular Production Build
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -64,7 +70,7 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 18
 | 
			
		||||
          node-version: 20
 | 
			
		||||
 | 
			
		||||
      - name: Write .npmrc file
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -86,7 +92,13 @@ jobs:
 | 
			
		||||
          CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: npm ci
 | 
			
		||||
        run: |
 | 
			
		||||
          cd client
 | 
			
		||||
          npm ci
 | 
			
		||||
          # Decrypt and Install sheet
 | 
			
		||||
          echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
 | 
			
		||||
          npm i ./libraries/sheet-crypto.tgz
 | 
			
		||||
          # End
 | 
			
		||||
 | 
			
		||||
      # Install pm2 and prepare SASJS server
 | 
			
		||||
      - run: npm i -g pm2
 | 
			
		||||
@@ -150,7 +162,7 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 18
 | 
			
		||||
          node-version: 20
 | 
			
		||||
 | 
			
		||||
      - name: Write .npmrc file
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -168,6 +180,9 @@ jobs:
 | 
			
		||||
          npm i -g @sasjs/cli
 | 
			
		||||
          # jq is used to parse the release JSON
 | 
			
		||||
          apt-get install jq -y
 | 
			
		||||
          # doxygen is used for the SASJS docs
 | 
			
		||||
          apt-get update
 | 
			
		||||
          apt-get install doxygen -y
 | 
			
		||||
 | 
			
		||||
      - name: Create Empty Release (assets are posted later)
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -182,6 +197,10 @@ jobs:
 | 
			
		||||
        run: |
 | 
			
		||||
          cd client
 | 
			
		||||
          npm ci
 | 
			
		||||
          # Decrypt and Install sheet
 | 
			
		||||
          echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
 | 
			
		||||
          npm i ./libraries/sheet-crypto.tgz
 | 
			
		||||
          # End
 | 
			
		||||
          npm run build
 | 
			
		||||
 | 
			
		||||
      - name: Build SAS9 EBI Release
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -11,6 +11,7 @@ client/cypress/screenshots
 | 
			
		||||
client/cypress/results
 | 
			
		||||
client/cypress/videos
 | 
			
		||||
client/documentation
 | 
			
		||||
client/sheet-crypto*
 | 
			
		||||
cypress.env.json
 | 
			
		||||
sasjsbuild
 | 
			
		||||
sasjsresults
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										35
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -1,18 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
    "cSpell.words": [
 | 
			
		||||
        "SYSERRORTEXT",
 | 
			
		||||
        "SYSWARNINGTEXT"
 | 
			
		||||
    ],
 | 
			
		||||
    "editor.rulers": [
 | 
			
		||||
        80
 | 
			
		||||
    ],
 | 
			
		||||
    "files.trimTrailingWhitespace": true,
 | 
			
		||||
    "[markdown]": {
 | 
			
		||||
        "files.trimTrailingWhitespace": false
 | 
			
		||||
    },
 | 
			
		||||
    "workbench.colorCustomizations": {
 | 
			
		||||
        "titleBar.activeForeground": "#ebe8e8",
 | 
			
		||||
        "titleBar.activeBackground": "#95ff0053",
 | 
			
		||||
    },
 | 
			
		||||
    "terminal.integrated.wordSeparators": " ()[]{}',\"`─‘’"
 | 
			
		||||
}
 | 
			
		||||
  "cSpell.words": [
 | 
			
		||||
    "Licence",
 | 
			
		||||
    "SYSERRORTEXT",
 | 
			
		||||
    "SYSWARNINGTEXT",
 | 
			
		||||
    "xlmaprules",
 | 
			
		||||
    "xlmaps"
 | 
			
		||||
  ],
 | 
			
		||||
  "editor.rulers": [80],
 | 
			
		||||
  "files.trimTrailingWhitespace": true,
 | 
			
		||||
  "[markdown]": {
 | 
			
		||||
    "files.trimTrailingWhitespace": false
 | 
			
		||||
  },
 | 
			
		||||
  "workbench.colorCustomizations": {
 | 
			
		||||
    "titleBar.activeForeground": "#ebe8e8",
 | 
			
		||||
    "titleBar.activeBackground": "#95ff0053"
 | 
			
		||||
  },
 | 
			
		||||
  "terminal.integrated.wordSeparators": " ()[]{}',\"`─‘’"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										113
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,3 +1,116 @@
 | 
			
		||||
# [6.7.0](https://git.datacontroller.io/dc/dc/compare/v6.6.4...v6.7.0) (2024-04-01)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* numeric values in hot dropdown aligned right ([9635626](https://git.datacontroller.io/dc/dc/commit/963562621ddf0e8d24a29a8481c5e6da1b040708))
 | 
			
		||||
 | 
			
		||||
## [6.6.4](https://git.datacontroller.io/dc/dc/compare/v6.6.3...v6.6.4) (2024-04-01)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* ordering SOFTSELECT numerically in dropdown ([f522038](https://git.datacontroller.io/dc/dc/commit/f522038b8ddb1da14b8adbf8346d0a4539a94cc8)), closes [#85](https://git.datacontroller.io/dc/dc/issues/85)
 | 
			
		||||
* reverting col ([fbbcf90](https://git.datacontroller.io/dc/dc/commit/fbbcf90956bf538b032b0107c07b8576d20353b9))
 | 
			
		||||
* typo ([31d4e5c](https://git.datacontroller.io/dc/dc/commit/31d4e5c727f790d428fb2ea8da60dca929561805))
 | 
			
		||||
 | 
			
		||||
## [6.6.3](https://git.datacontroller.io/dc/dc/compare/v6.6.2...v6.6.3) (2024-02-26)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* allow empty clause value when NE or CONTAINS ([432450a](https://git.datacontroller.io/dc/dc/commit/432450a15b51a269821ba1d430854f5d1dd04703))
 | 
			
		||||
 | 
			
		||||
## [6.6.2](https://git.datacontroller.io/dc/dc/compare/v6.6.1...v6.6.2) (2024-02-22)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* excel with commas getting wrapped in quotes ([3860134](https://git.datacontroller.io/dc/dc/commit/38601346a529cfe3787bb286a639e0293c365020))
 | 
			
		||||
 | 
			
		||||
## [6.6.1](https://git.datacontroller.io/dc/dc/compare/v6.6.0...v6.6.1) (2024-02-19)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **client:** bumped @sasjs/adapter with fixed redirected login ([eb1c09d](https://git.datacontroller.io/dc/dc/commit/eb1c09d7909ba07faf763da261545dc1efaec1b3))
 | 
			
		||||
 | 
			
		||||
# [6.6.0](https://git.datacontroller.io/dc/dc/compare/v6.5.2...v6.6.0) (2024-02-12)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* adjust the col numbers in extracted data ([cff5989](https://git.datacontroller.io/dc/dc/commit/cff598955930d2581349e5c6e8b2dd3f9ac96b4c))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* extra table metadata for [#75](https://git.datacontroller.io/dc/dc/issues/75) ([837821f](https://git.datacontroller.io/dc/dc/commit/837821fd01477d340524dfdaf8dd3d3758cf3095))
 | 
			
		||||
* show dsnote on hover title ([6565834](https://git.datacontroller.io/dc/dc/commit/6565834ad4089ecf2de39967e6ed6f217ee4a0a5))
 | 
			
		||||
 | 
			
		||||
## [6.5.2](https://git.datacontroller.io/dc/dc/compare/v6.5.1...v6.5.2) (2024-02-06)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* ordering mpe_selectbox data by the data values after selectbox_order ([2b54034](https://git.datacontroller.io/dc/dc/commit/2b5403497317632a4be8a00f21455c036f1e6461))
 | 
			
		||||
 | 
			
		||||
## [6.5.1](https://git.datacontroller.io/dc/dc/compare/v6.5.0...v6.5.1) (2024-02-02)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* ensuring submitter email can be pulled from mpe_emails ([eac0104](https://git.datacontroller.io/dc/dc/commit/eac0104d7aebaf98ff1d1c504c1ce3b25d4a0ce8))
 | 
			
		||||
 | 
			
		||||
# [6.5.0](https://git.datacontroller.io/dc/dc/compare/v6.4.0...v6.5.0) (2024-01-26)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* filtering by reference to Variables as well as Values ([6eb1aa8](https://git.datacontroller.io/dc/dc/commit/6eb1aa85d29294d63e6af377e622fbed7fd1fab8))
 | 
			
		||||
 | 
			
		||||
# [6.4.0](https://git.datacontroller.io/dc/dc/compare/v6.3.1...v6.4.0) (2024-01-24)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* add dcLib to globals ([5d93346](https://git.datacontroller.io/dc/dc/commit/5d93346b52eda27c2829770e96686a713296d373))
 | 
			
		||||
* add service to get xlmap rules and fixed interface name ([9ffa30a](https://git.datacontroller.io/dc/dc/commit/9ffa30ab747f5b62acbd452431a5e6e440afcb80))
 | 
			
		||||
* increasing length of mpe_excel_map cols to ([2d4d068](https://git.datacontroller.io/dc/dc/commit/2d4d068413dcdac98581f08939e74bde65b73428))
 | 
			
		||||
* providing info on mapids to FE ([fd94945](https://git.datacontroller.io/dc/dc/commit/fd94945466c1a797ddc89815258a65624a9cb0cf))
 | 
			
		||||
* removing tables from EDIT menu that are in xlmaps ([9550ae4](https://git.datacontroller.io/dc/dc/commit/9550ae4d1154a0272f8a2427ac9d2afdfd699c96))
 | 
			
		||||
* removing XLMAP_TARGETLIBDS from mpe_xlmaps_rules table ([93702c6](https://git.datacontroller.io/dc/dc/commit/93702c63dc280cdba1e46f0fd8fe0deaec879611))
 | 
			
		||||
* renaming TABLE macvar to LOAD_REF in postdata.sas ([01915a2](https://git.datacontroller.io/dc/dc/commit/01915a2db9a4dfb94e4e8213e2c32181da36d349))
 | 
			
		||||
* reverting xlmap in getdata change ([2d6e747](https://git.datacontroller.io/dc/dc/commit/2d6e747db9b84e9fb0dfcf9102a2f7dd2cb51891))
 | 
			
		||||
* update edit tab to load ([516e5a2](https://git.datacontroller.io/dc/dc/commit/516e5a206216f79ab1dce9f4eab0d31115743160))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* adding ability to define the target table for excel maps ([c86fba9](https://git.datacontroller.io/dc/dc/commit/c86fba9dc75ddc6033132f469ad1c31b9131b12e))
 | 
			
		||||
* adding ismap attribute to getdata response (and fixing test) ([2702bb3](https://git.datacontroller.io/dc/dc/commit/2702bb3c84c45903def1aa2b8cc20a6dd080281b))
 | 
			
		||||
* Complex Excel Uploads ([cf19381](https://git.datacontroller.io/dc/dc/commit/cf193810606f287b8d6f864c4eb64d43c5ab5f3c)), closes [#69](https://git.datacontroller.io/dc/dc/issues/69)
 | 
			
		||||
* Create Tables / Files dropdown under load tab ([b473b19](https://git.datacontroller.io/dc/dc/commit/b473b198a61f468dff74cd8e64692e7847084a80))
 | 
			
		||||
* display list of maps in sidebar ([5aec024](https://git.datacontroller.io/dc/dc/commit/5aec0242429942f8a989b5fb79f8d3865e9de01a))
 | 
			
		||||
* implemented the logic for xlmap component ([50696bb](https://git.datacontroller.io/dc/dc/commit/50696bb926dd00472db65a008771a4b6352871be))
 | 
			
		||||
* model changes for [#69](https://git.datacontroller.io/dc/dc/issues/69) ([271543a](https://git.datacontroller.io/dc/dc/commit/271543a446a2116718f99f0540e3cd911f9f5fe7))
 | 
			
		||||
* new getxlmaps service to return rules for a particular xlmap_id ([56264ec](https://git.datacontroller.io/dc/dc/commit/56264ecc6908bf6c8e3e666dfeba7068d6195df8))
 | 
			
		||||
* validating the excel map after stage (adding load-ref) ([a485c3b](https://git.datacontroller.io/dc/dc/commit/a485c3b78724a36f7bacb264fb02140cc62d6512))
 | 
			
		||||
 | 
			
		||||
## [6.3.1](https://git.datacontroller.io/dc/dc/compare/v6.3.0...v6.3.1) (2024-01-01)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* enabling excel uploads to tables with retained keys, also adding more validation to MPE_TABLES updates ([3efccc4](https://git.datacontroller.io/dc/dc/commit/3efccc4cf3752763d049836724f2491c287f65db))
 | 
			
		||||
 | 
			
		||||
# [6.3.0](https://git.datacontroller.io/dc/dc/compare/v6.2.8...v6.3.0) (2023-12-04)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* viewer row handle ([dadac4f](https://git.datacontroller.io/dc/dc/commit/dadac4f13f85b5446198b6340cad28844defc94d))
 | 
			
		||||
 | 
			
		||||
## [6.2.8](https://git.datacontroller.io/dc/dc/compare/v6.2.7...v6.2.8) (2023-12-04)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -27,4 +27,6 @@ For more information:
 | 
			
		||||
 | 
			
		||||
* Main site:  https://datacontroller.io
 | 
			
		||||
* Docs:  https://docs.datacontroller.io
 | 
			
		||||
* Code: https://code.datacontroller.io
 | 
			
		||||
* Code: https://code.datacontroller.io
 | 
			
		||||
 | 
			
		||||
For support, contact support@4gl.io or reach out on [Matrix](https://matrix.to/#/#dc:4gl.io)!
 | 
			
		||||
@@ -45,6 +45,7 @@
 | 
			
		||||
              "numbro",
 | 
			
		||||
              "@clr/icons",
 | 
			
		||||
              "@sasjs/adapter",
 | 
			
		||||
              "@sasjs/utils/types/serverType",
 | 
			
		||||
              "@sasjs/utils/input/validators",
 | 
			
		||||
              "@sasjs/utils/utils/bytesToSize",
 | 
			
		||||
              "base64-arraybuffer",
 | 
			
		||||
@@ -67,7 +68,6 @@
 | 
			
		||||
              "src/styles.scss"
 | 
			
		||||
            ],
 | 
			
		||||
            "scripts": [
 | 
			
		||||
              "node_modules/@clr/icons/clr-icons.min.js",
 | 
			
		||||
              "node_modules/marked/marked.min.js"
 | 
			
		||||
            ]
 | 
			
		||||
          },
 | 
			
		||||
@@ -116,10 +116,10 @@
 | 
			
		||||
          "builder": "@angular-devkit/build-angular:dev-server",
 | 
			
		||||
          "configurations": {
 | 
			
		||||
            "production": {
 | 
			
		||||
              "browserTarget": "datacontroller:build:production"
 | 
			
		||||
              "buildTarget": "datacontroller:build:production"
 | 
			
		||||
            },
 | 
			
		||||
            "development": {
 | 
			
		||||
              "browserTarget": "datacontroller:build:development"
 | 
			
		||||
              "buildTarget": "datacontroller:build:development"
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          "defaultConfiguration": "development"
 | 
			
		||||
@@ -127,31 +127,27 @@
 | 
			
		||||
        "extract-i18n": {
 | 
			
		||||
          "builder": "@angular-devkit/build-angular:extract-i18n",
 | 
			
		||||
          "options": {
 | 
			
		||||
            "browserTarget": "datacontroller:build"
 | 
			
		||||
            "buildTarget": "datacontroller:build"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "test": {
 | 
			
		||||
          "builder": "@angular-devkit/build-angular:karma",
 | 
			
		||||
          "options": {
 | 
			
		||||
            "tsConfig": "tsconfig.spec.json",
 | 
			
		||||
            "inlineStyleLanguage": "scss",
 | 
			
		||||
            "codeCoverage": true,
 | 
			
		||||
            "polyfills": [
 | 
			
		||||
              "src/polyfills.ts",
 | 
			
		||||
              "zone.js",
 | 
			
		||||
              "zone.js/testing"
 | 
			
		||||
            ],
 | 
			
		||||
            "styles": [
 | 
			
		||||
              "src/styles.scss"
 | 
			
		||||
            ],
 | 
			
		||||
            "scripts": [
 | 
			
		||||
 | 
			
		||||
            ],
 | 
			
		||||
            "tsConfig": "tsconfig.spec.json",
 | 
			
		||||
            "inlineStyleLanguage": "scss",
 | 
			
		||||
            "assets": [
 | 
			
		||||
              "src/favicon.ico",
 | 
			
		||||
              "src/assets"
 | 
			
		||||
            ],
 | 
			
		||||
            "karmaConfig": "karma.conf.js"
 | 
			
		||||
            "styles": [
 | 
			
		||||
              "src/styles.scss"
 | 
			
		||||
            ],
 | 
			
		||||
            "scripts": []
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "lint": {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,8 @@ export default defineConfig({
 | 
			
		||||
    html: true,
 | 
			
		||||
    json: false,
 | 
			
		||||
  },
 | 
			
		||||
  viewportHeight: 900,
 | 
			
		||||
  viewportWidth: 1600,
 | 
			
		||||
 | 
			
		||||
  chromeWebSecurity: false,
 | 
			
		||||
  defaultCommandTimeout: 30000,
 | 
			
		||||
 
 | 
			
		||||
@@ -221,13 +221,13 @@ const submitExcel = (callback?: any) => {
 | 
			
		||||
 | 
			
		||||
const rejectExcel = (callback?: any) => {
 | 
			
		||||
  cy.get('button', { timeout: longerCommandTimeout })
 | 
			
		||||
    .should('contain', 'Go to approvals screen')
 | 
			
		||||
    .should('contain', 'Approve')
 | 
			
		||||
    .then((allButtons: any) => {
 | 
			
		||||
      for (let approvalButton of allButtons) {
 | 
			
		||||
        if (
 | 
			
		||||
          approvalButton.innerText
 | 
			
		||||
            .toLowerCase()
 | 
			
		||||
            .includes('go to approvals screen')
 | 
			
		||||
            .includes('approve')
 | 
			
		||||
        ) {
 | 
			
		||||
          approvalButton.click()
 | 
			
		||||
          break
 | 
			
		||||
 
 | 
			
		||||
@@ -405,13 +405,13 @@ const submitExcel = (callback?: any) => {
 | 
			
		||||
 | 
			
		||||
const rejectExcel = (callback?: any) => {
 | 
			
		||||
  cy.get('button', { timeout: longerCommandTimeout })
 | 
			
		||||
    .should('contain', 'Go to approvals screen')
 | 
			
		||||
    .should('contain', 'Approve')
 | 
			
		||||
    .then((allButtons: any) => {
 | 
			
		||||
      for (let approvalButton of allButtons) {
 | 
			
		||||
        if (
 | 
			
		||||
          approvalButton.innerText
 | 
			
		||||
            .toLowerCase()
 | 
			
		||||
            .includes('go to approvals screen')
 | 
			
		||||
            .includes('approve')
 | 
			
		||||
        ) {
 | 
			
		||||
          approvalButton.click()
 | 
			
		||||
          break
 | 
			
		||||
@@ -438,13 +438,13 @@ const rejectExcel = (callback?: any) => {
 | 
			
		||||
 | 
			
		||||
const acceptExcel = (callback?: any) => {
 | 
			
		||||
  cy.get('button', { timeout: longerCommandTimeout })
 | 
			
		||||
    .should('contain', 'Go to approvals screen')
 | 
			
		||||
    .should('contain', 'Approve')
 | 
			
		||||
    .then((allButtons: any) => {
 | 
			
		||||
      for (let approvalButton of allButtons) {
 | 
			
		||||
        if (
 | 
			
		||||
          approvalButton.innerText
 | 
			
		||||
            .toLowerCase()
 | 
			
		||||
            .includes('go to approvals screen')
 | 
			
		||||
            .includes('approve')
 | 
			
		||||
        ) {
 | 
			
		||||
          approvalButton.click()
 | 
			
		||||
          break
 | 
			
		||||
 
 | 
			
		||||
@@ -699,13 +699,13 @@ const submitTable = (callback?: any) => {
 | 
			
		||||
 | 
			
		||||
const approveTable = (callback?: any) => {
 | 
			
		||||
  cy.get('button', { timeout: longerCommandTimeout })
 | 
			
		||||
    .should('contain', 'Go to approvals screen')
 | 
			
		||||
    .should('contain', 'Approve')
 | 
			
		||||
    .then((allButtons: any) => {
 | 
			
		||||
      for (let approvalButton of allButtons) {
 | 
			
		||||
        if (
 | 
			
		||||
          approvalButton.innerText
 | 
			
		||||
            .toLowerCase()
 | 
			
		||||
            .includes('go to approvals screen')
 | 
			
		||||
            .includes('approve')
 | 
			
		||||
        ) {
 | 
			
		||||
          approvalButton.click()
 | 
			
		||||
          break
 | 
			
		||||
 
 | 
			
		||||
@@ -125,13 +125,13 @@ const submitExcel = (callback?: any) => {
 | 
			
		||||
 | 
			
		||||
const rejectExcel = (callback?: any) => {
 | 
			
		||||
  cy.get('button', { timeout: longerCommandTimeout })
 | 
			
		||||
    .should('contain', 'Go to approvals screen')
 | 
			
		||||
    .should('contain', 'Approve')
 | 
			
		||||
    .then((allButtons: any) => {
 | 
			
		||||
      for (let approvalButton of allButtons) {
 | 
			
		||||
        if (
 | 
			
		||||
          approvalButton.innerText
 | 
			
		||||
            .toLowerCase()
 | 
			
		||||
            .includes('go to approvals screen')
 | 
			
		||||
            .includes('approve')
 | 
			
		||||
        ) {
 | 
			
		||||
          approvalButton.click()
 | 
			
		||||
          break
 | 
			
		||||
 
 | 
			
		||||
@@ -221,14 +221,10 @@ const submitExcel = (callback?: any) => {
 | 
			
		||||
 | 
			
		||||
const rejectExcel = (callback?: any) => {
 | 
			
		||||
  cy.get('button', { timeout: longerCommandTimeout })
 | 
			
		||||
    .should('contain', 'Go to approvals screen')
 | 
			
		||||
    .should('contain', 'Approve')
 | 
			
		||||
    .then((allButtons: any) => {
 | 
			
		||||
      for (let approvalButton of allButtons) {
 | 
			
		||||
        if (
 | 
			
		||||
          approvalButton.innerText
 | 
			
		||||
            .toLowerCase()
 | 
			
		||||
            .includes('go to approvals screen')
 | 
			
		||||
        ) {
 | 
			
		||||
        if (approvalButton.innerText.toLowerCase().includes('approve')) {
 | 
			
		||||
          approvalButton.click()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -407,14 +407,10 @@ const submitExcel = (callback?: any) => {
 | 
			
		||||
 | 
			
		||||
const rejectExcel = (callback?: any) => {
 | 
			
		||||
  cy.get('button', { timeout: longerCommandTimeout })
 | 
			
		||||
    .should('contain', 'Go to approvals screen')
 | 
			
		||||
    .should('contain', 'Approve')
 | 
			
		||||
    .then((allButtons: any) => {
 | 
			
		||||
      for (let approvalButton of allButtons) {
 | 
			
		||||
        if (
 | 
			
		||||
          approvalButton.innerText
 | 
			
		||||
            .toLowerCase()
 | 
			
		||||
            .includes('go to approvals screen')
 | 
			
		||||
        ) {
 | 
			
		||||
        if (approvalButton.innerText.toLowerCase().includes('approve')) {
 | 
			
		||||
          approvalButton.click()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
@@ -440,14 +436,10 @@ const rejectExcel = (callback?: any) => {
 | 
			
		||||
 | 
			
		||||
const acceptExcel = (callback?: any) => {
 | 
			
		||||
  cy.get('button', { timeout: longerCommandTimeout })
 | 
			
		||||
    .should('contain', 'Go to approvals screen')
 | 
			
		||||
    .should('contain', 'Approve')
 | 
			
		||||
    .then((allButtons: any) => {
 | 
			
		||||
      for (let approvalButton of allButtons) {
 | 
			
		||||
        if (
 | 
			
		||||
          approvalButton.innerText
 | 
			
		||||
            .toLowerCase()
 | 
			
		||||
            .includes('go to approvals screen')
 | 
			
		||||
        ) {
 | 
			
		||||
        if (approvalButton.innerText.toLowerCase().includes('approve')) {
 | 
			
		||||
          approvalButton.click()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -699,14 +699,10 @@ const submitTable = (callback?: any) => {
 | 
			
		||||
 | 
			
		||||
const approveTable = (callback?: any) => {
 | 
			
		||||
  cy.get('button', { timeout: longerCommandTimeout })
 | 
			
		||||
    .should('contain', 'Go to approvals screen')
 | 
			
		||||
    .should('contain', 'Approve')
 | 
			
		||||
    .then((allButtons: any) => {
 | 
			
		||||
      for (let approvalButton of allButtons) {
 | 
			
		||||
        if (
 | 
			
		||||
          approvalButton.innerText
 | 
			
		||||
            .toLowerCase()
 | 
			
		||||
            .includes('go to approvals screen')
 | 
			
		||||
        ) {
 | 
			
		||||
        if (approvalButton.innerText.toLowerCase().includes('approve')) {
 | 
			
		||||
          approvalButton.click()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -125,14 +125,10 @@ const submitExcel = (callback?: any) => {
 | 
			
		||||
 | 
			
		||||
const rejectExcel = (callback?: any) => {
 | 
			
		||||
  cy.get('button', { timeout: longerCommandTimeout })
 | 
			
		||||
    .should('contain', 'Go to approvals screen')
 | 
			
		||||
    .should('contain', 'Approve')
 | 
			
		||||
    .then((allButtons: any) => {
 | 
			
		||||
      for (let approvalButton of allButtons) {
 | 
			
		||||
        if (
 | 
			
		||||
          approvalButton.innerText
 | 
			
		||||
            .toLowerCase()
 | 
			
		||||
            .includes('go to approvals screen')
 | 
			
		||||
        ) {
 | 
			
		||||
        if (approvalButton.innerText.toLowerCase().includes('approve')) {
 | 
			
		||||
          approvalButton.click()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,45 +0,0 @@
 | 
			
		||||
// Karma configuration file, see link for more information
 | 
			
		||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
 | 
			
		||||
 | 
			
		||||
module.exports = function (config) {
 | 
			
		||||
  config.set({
 | 
			
		||||
    basePath: '',
 | 
			
		||||
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
 | 
			
		||||
    plugins: [
 | 
			
		||||
      require('karma-jasmine'),
 | 
			
		||||
      require('karma-chrome-launcher'),
 | 
			
		||||
      require('karma-jasmine-html-reporter'),
 | 
			
		||||
      require('karma-coverage'),
 | 
			
		||||
      require('@angular-devkit/build-angular/plugins/karma')
 | 
			
		||||
    ],
 | 
			
		||||
    client: {
 | 
			
		||||
      jasmine: {
 | 
			
		||||
        // you can add configuration options for Jasmine here
 | 
			
		||||
        // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
 | 
			
		||||
        // for example, you can disable the random execution with `random: false`
 | 
			
		||||
        // or set a specific seed with `seed: 4321`
 | 
			
		||||
      },
 | 
			
		||||
      clearContext: false // leave Jasmine Spec Runner output visible in browser
 | 
			
		||||
    },
 | 
			
		||||
    jasmineHtmlReporter: {
 | 
			
		||||
      suppressAll: true // removes the duplicated traces
 | 
			
		||||
    },
 | 
			
		||||
    coverageReporter: {
 | 
			
		||||
      dir: require('path').join(__dirname, './coverage/datacontroller'),
 | 
			
		||||
      subdir: '.',
 | 
			
		||||
      reporters: [
 | 
			
		||||
        { type: 'html' },
 | 
			
		||||
        { type: 'text-summary' }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    reporters: ['progress', 'kjhtml'],
 | 
			
		||||
    browsers: ['Chrome', 'ChromeHeadless', 'ChromeHeadlessCI'],
 | 
			
		||||
    restartOnFileChange: true,
 | 
			
		||||
    customLaunchers: {
 | 
			
		||||
      ChromeHeadlessCI: {
 | 
			
		||||
        base: 'ChromeHeadless',
 | 
			
		||||
        flags: ['--no-sandbox']
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								client/libraries/sheet-crypto.tgz.gpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								client/libraries/sheet-crypto.tgz.gpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -10,7 +10,7 @@ const check = (cwd) => {
 | 
			
		||||
        onlyAllow:
 | 
			
		||||
          'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
 | 
			
		||||
        excludePackages:
 | 
			
		||||
          '@cds/city@1.1.0;@handsontable/angular@13.1.0;handsontable@13.1.0;hyperformula@2.5.0;jackspeak@2.2.0;path-scurry@1.7.0'
 | 
			
		||||
          '@cds/city@1.1.0;@handsontable/angular@13.1.0;handsontable@13.1.0;hyperformula@2.7.0;jackspeak@2.2.0;path-scurry@1.7.0'
 | 
			
		||||
      },
 | 
			
		||||
      (error, json) => {
 | 
			
		||||
        if (error) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25590
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										25590
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -18,8 +18,8 @@
 | 
			
		||||
    "deploy_sasjs": "rsync -avhe ssh ./dist/* --delete root@${npm_config_account}.4gl.io:/var/www/html/dc/dev",
 | 
			
		||||
    "viyabuild": "cd build; ./viyabuild.sh",
 | 
			
		||||
    "lint": "cd .. && npm run lint",
 | 
			
		||||
    "test": "ng test",
 | 
			
		||||
    "test:headless": "ng test --browsers ChromeHeadless",
 | 
			
		||||
    "test": "npx ng test",
 | 
			
		||||
    "test:headless": "npx ng test --no-watch --no-progress --browsers ChromeHeadless",
 | 
			
		||||
    "watch": "ng test watch=true",
 | 
			
		||||
    "pree2e": "webdriver-manager update",
 | 
			
		||||
    "e2e": "protractor protractor.config.js",
 | 
			
		||||
@@ -35,23 +35,22 @@
 | 
			
		||||
  },
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@angular/animations": "^16.1.2",
 | 
			
		||||
    "@angular/cdk": "^15.2.0",
 | 
			
		||||
    "@angular/common": "^16.1.2",
 | 
			
		||||
    "@angular/compiler": "^16.1.2",
 | 
			
		||||
    "@angular/core": "^16.1.2",
 | 
			
		||||
    "@angular/forms": "^16.1.2",
 | 
			
		||||
    "@angular/platform-browser": "^16.1.2",
 | 
			
		||||
    "@angular/platform-browser-dynamic": "^16.1.2",
 | 
			
		||||
    "@angular/router": "^16.1.2",
 | 
			
		||||
    "@cds/core": "^6.4.2",
 | 
			
		||||
    "@clr/angular": "^13.17.0",
 | 
			
		||||
    "@angular/animations": "^17.3.3",
 | 
			
		||||
    "@angular/cdk": "^17.3.3",
 | 
			
		||||
    "@angular/common": "^17.3.3",
 | 
			
		||||
    "@angular/compiler": "^17.3.3",
 | 
			
		||||
    "@angular/core": "^17.3.3",
 | 
			
		||||
    "@angular/forms": "^17.3.3",
 | 
			
		||||
    "@angular/platform-browser": "^17.3.3",
 | 
			
		||||
    "@angular/platform-browser-dynamic": "^17.3.3",
 | 
			
		||||
    "@angular/router": "^17.3.3",
 | 
			
		||||
    "@cds/core": "^6.10.0",
 | 
			
		||||
    "@clr/angular": "^17.0.1",
 | 
			
		||||
    "@clr/icons": "^13.0.2",
 | 
			
		||||
    "@clr/ui": "^13.17.0",
 | 
			
		||||
    "@clr/ui": "^17.0.1",
 | 
			
		||||
    "@handsontable/angular": "^13.1.0",
 | 
			
		||||
    "@sasjs/adapter": "4.10.1",
 | 
			
		||||
    "@sasjs/adapter": "4.10.2",
 | 
			
		||||
    "@sasjs/utils": "^3.4.0",
 | 
			
		||||
    "@sheet/crypto": "1.20211122.1",
 | 
			
		||||
    "@types/d3-graphviz": "^2.6.7",
 | 
			
		||||
    "@types/text-encoding": "0.0.35",
 | 
			
		||||
    "base64-arraybuffer": "^0.2.0",
 | 
			
		||||
@@ -78,24 +77,25 @@
 | 
			
		||||
    "stream-http": "3.2.0",
 | 
			
		||||
    "text-encoding": "^0.7.0",
 | 
			
		||||
    "tslib": "^2.3.0",
 | 
			
		||||
    "zone.js": "~0.13.0"
 | 
			
		||||
    "vm": "^0.1.0",
 | 
			
		||||
    "zone.js": "~0.14.4"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@angular-devkit/build-angular": "^16.1.0",
 | 
			
		||||
    "@angular-eslint/builder": "16.0.3",
 | 
			
		||||
    "@angular-eslint/eslint-plugin": "16.0.3",
 | 
			
		||||
    "@angular-eslint/eslint-plugin-template": "16.0.3",
 | 
			
		||||
    "@angular-eslint/schematics": "16.0.3",
 | 
			
		||||
    "@angular-eslint/template-parser": "16.0.3",
 | 
			
		||||
    "@angular/cli": "^16.1.0",
 | 
			
		||||
    "@angular/compiler-cli": "^16.1.2",
 | 
			
		||||
    "@angular-devkit/build-angular": "^17.3.3",
 | 
			
		||||
    "@angular-eslint/builder": "17.3.0",
 | 
			
		||||
    "@angular-eslint/eslint-plugin": "17.3.0",
 | 
			
		||||
    "@angular-eslint/eslint-plugin-template": "17.3.0",
 | 
			
		||||
    "@angular-eslint/schematics": "17.3.0",
 | 
			
		||||
    "@angular-eslint/template-parser": "17.3.0",
 | 
			
		||||
    "@angular/cli": "^17.3.3",
 | 
			
		||||
    "@angular/compiler-cli": "^17.3.3",
 | 
			
		||||
    "@babel/plugin-proposal-private-methods": "^7.18.6",
 | 
			
		||||
    "@compodoc/compodoc": "^1.1.21",
 | 
			
		||||
    "@cypress/webpack-preprocessor": "^5.17.1",
 | 
			
		||||
    "@types/core-js": "^2.5.5",
 | 
			
		||||
    "@types/crypto-js": "^4.2.1",
 | 
			
		||||
    "@types/es6-shim": "^0.31.39",
 | 
			
		||||
    "@types/jasmine": "~3.6.0",
 | 
			
		||||
    "@types/jasmine": "~5.1.4",
 | 
			
		||||
    "@types/lodash-es": "^4.17.3",
 | 
			
		||||
    "@types/marked": "^4.3.0",
 | 
			
		||||
    "@types/node": "12.20.50",
 | 
			
		||||
@@ -109,12 +109,12 @@
 | 
			
		||||
    "es6-shim": "^0.35.5",
 | 
			
		||||
    "eslint": "^8.33.0",
 | 
			
		||||
    "git-describe": "^4.0.4",
 | 
			
		||||
    "jasmine-core": "~3.6.0",
 | 
			
		||||
    "karma": "~6.3.0",
 | 
			
		||||
    "karma-chrome-launcher": "~3.1.0",
 | 
			
		||||
    "karma-coverage": "~2.1.0",
 | 
			
		||||
    "karma-jasmine": "~4.0.0",
 | 
			
		||||
    "karma-jasmine-html-reporter": "~1.7.0",
 | 
			
		||||
    "jasmine-core": "~5.1.2",
 | 
			
		||||
    "karma": "~6.4.3",
 | 
			
		||||
    "karma-chrome-launcher": "~3.2.0",
 | 
			
		||||
    "karma-coverage": "~2.2.1",
 | 
			
		||||
    "karma-jasmine": "~5.1.0",
 | 
			
		||||
    "karma-jasmine-html-reporter": "~2.1.0",
 | 
			
		||||
    "license-checker": "25.0.1",
 | 
			
		||||
    "lodash-es": "^4.17.21",
 | 
			
		||||
    "mochawesome": "^7.1.3",
 | 
			
		||||
@@ -123,9 +123,7 @@
 | 
			
		||||
    "rimraf": "3.0.2",
 | 
			
		||||
    "ts-loader": "^9.2.8",
 | 
			
		||||
    "ts-node": "^3.3.0",
 | 
			
		||||
    "typedoc": "^0.24.8",
 | 
			
		||||
    "typedoc-plugin-external-module-name": "^4.0.6",
 | 
			
		||||
    "typescript": "~4.9.4",
 | 
			
		||||
    "typescript": "~5.4.4",
 | 
			
		||||
    "wait-on": "^6.0.1",
 | 
			
		||||
    "watch": "^1.0.2"
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,12 @@ export const initFilter: { filter: FilterCache } = {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface XLMapListItem {
 | 
			
		||||
  id: string
 | 
			
		||||
  description: string
 | 
			
		||||
  targetDS: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Cached filtering values across whole app (editor, viewer, viewboxes)
 | 
			
		||||
 * Cached lineage libraries, tables
 | 
			
		||||
@@ -46,6 +52,8 @@ export const initFilter: { filter: FilterCache } = {
 | 
			
		||||
 */
 | 
			
		||||
export const globals: {
 | 
			
		||||
  rootParam: string
 | 
			
		||||
  dcLib: string
 | 
			
		||||
  xlmaps: XLMapListItem[]
 | 
			
		||||
  editor: any
 | 
			
		||||
  viewer: any
 | 
			
		||||
  viewboxes: ViewboxCache
 | 
			
		||||
@@ -57,11 +65,13 @@ export const globals: {
 | 
			
		||||
  [key: string]: any
 | 
			
		||||
} = {
 | 
			
		||||
  rootParam: <string>'',
 | 
			
		||||
  dcLib: '',
 | 
			
		||||
  xlmaps: [],
 | 
			
		||||
  editor: {
 | 
			
		||||
    startupSet: <boolean>false,
 | 
			
		||||
    treeNodeLibraries: <any[] | null>[],
 | 
			
		||||
    libsAndTables: <any[]>[],
 | 
			
		||||
    libraries: <String[] | undefined>[],
 | 
			
		||||
    libraries: <string[] | undefined>[],
 | 
			
		||||
    library: <string>'',
 | 
			
		||||
    table: <string>'',
 | 
			
		||||
    filter: <FilterCache>{
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@
 | 
			
		||||
        <div class="alert-items">
 | 
			
		||||
          <div class="alert-item static">
 | 
			
		||||
            <div class="alert-icon-wrapper">
 | 
			
		||||
              <clr-icon class="mt-2" shape="warning-standard"></clr-icon>
 | 
			
		||||
              <cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="alert-text">
 | 
			
		||||
              Data Controller (FREE Tier) - to upgrade contact
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
        <div class="alert-items">
 | 
			
		||||
          <div class="alert-item static">
 | 
			
		||||
            <div class="alert-icon-wrapper">
 | 
			
		||||
              <clr-icon class="mt-2" shape="warning-standard"></clr-icon>
 | 
			
		||||
              <cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="alert-text">
 | 
			
		||||
              Data Controller (FREE Tier) - Problem with licence
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
      <div class="alert-items">
 | 
			
		||||
        <div class="alert-item static">
 | 
			
		||||
          <div class="alert-icon-wrapper">
 | 
			
		||||
            <clr-icon class="mt-2" shape="warning-standard"></clr-icon>
 | 
			
		||||
            <cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="alert-text">
 | 
			
		||||
@@ -85,7 +85,7 @@
 | 
			
		||||
      <div class="alert-items">
 | 
			
		||||
        <div class="alert-item static">
 | 
			
		||||
          <div class="alert-icon-wrapper">
 | 
			
		||||
            <clr-icon class="mt-2" shape="warning-standard"></clr-icon>
 | 
			
		||||
            <cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="alert-text">
 | 
			
		||||
@@ -168,7 +168,7 @@
 | 
			
		||||
          </button>
 | 
			
		||||
          <clr-dropdown-menu *clrIfOpen clrPosition="bottom-left">
 | 
			
		||||
            <a [routerLink]="['/view']" clrDropdownItem>VIEW</a>
 | 
			
		||||
            <a [routerLink]="['/home']" clrDropdownItem>EDIT</a>
 | 
			
		||||
            <a [routerLink]="['/home']" clrDropdownItem>LOAD</a>
 | 
			
		||||
            <a [routerLink]="['/review/submitted']" clrDropdownItem>REVIEW</a>
 | 
			
		||||
          </clr-dropdown-menu>
 | 
			
		||||
        </clr-dropdown>
 | 
			
		||||
@@ -189,7 +189,7 @@
 | 
			
		||||
            router.url.includes('edit-record') ||
 | 
			
		||||
            router.url.includes('home')
 | 
			
		||||
          "
 | 
			
		||||
          >EDIT</a
 | 
			
		||||
          >LOAD</a
 | 
			
		||||
        >
 | 
			
		||||
        <a
 | 
			
		||||
          [routerLink]="['/review/submitted']"
 | 
			
		||||
@@ -204,14 +204,7 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <div class="header-actions">
 | 
			
		||||
      <div class="nav-text">
 | 
			
		||||
        <app-loading-indicator></app-loading-indicator>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="dropdown">
 | 
			
		||||
        <app-user-nav-dropdown></app-user-nav-dropdown>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <app-header-actions></app-header-actions>
 | 
			
		||||
  </header>
 | 
			
		||||
  <nav
 | 
			
		||||
    *ngIf="
 | 
			
		||||
 
 | 
			
		||||
@@ -91,33 +91,12 @@ header {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nav
 | 
			
		||||
  .nav-link {
 | 
			
		||||
    color: #fafafa;
 | 
			
		||||
    opacity: .9;
 | 
			
		||||
    line-height: 1.45rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nav .nav-link:hover {
 | 
			
		||||
      box-shadow: inset 0 -3px 0 transparent;
 | 
			
		||||
      transition: box-shadow .2s ease-in;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nav
 | 
			
		||||
    .nav-link:hover {
 | 
			
		||||
  .nav-link:hover {
 | 
			
		||||
      color: #fafafa;
 | 
			
		||||
      opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nav .nav-link.active {
 | 
			
		||||
     background: #61717D;
 | 
			
		||||
      opacity: 1;
 | 
			
		||||
      box-shadow: inset 0 -3px transparent;
 | 
			
		||||
      // padding: 0 1rem 0 1rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nav .nav-item {
 | 
			
		||||
      margin-right: 1rem;
 | 
			
		||||
  .nav-link.active {
 | 
			
		||||
    background: #61717D;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,6 @@ import { NgxJsonViewerModule } from 'ngx-json-viewer'
 | 
			
		||||
    SharedModule,
 | 
			
		||||
    ClarityModule,
 | 
			
		||||
    AppSharedModule,
 | 
			
		||||
    HomeModule,
 | 
			
		||||
    PipesModule,
 | 
			
		||||
    DirectivesModule,
 | 
			
		||||
    NgxJsonViewerModule
 | 
			
		||||
 
 | 
			
		||||
@@ -4,19 +4,19 @@
 | 
			
		||||
 * The full license information can be found in LICENSE in the root directory of this project.
 | 
			
		||||
 */
 | 
			
		||||
import { ModuleWithProviders } from '@angular/core'
 | 
			
		||||
import { Routes, RouterModule } from '@angular/router'
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router'
 | 
			
		||||
 | 
			
		||||
import { HomeComponent } from './home/home.component'
 | 
			
		||||
import { NotFoundComponent } from './not-found/not-found.component'
 | 
			
		||||
 | 
			
		||||
import { DeployModule } from './deploy/deploy.module'
 | 
			
		||||
import { EditorModule } from './editor/editor.module'
 | 
			
		||||
import { HomeModule } from './home/home.module'
 | 
			
		||||
import { LicensingModule } from './licensing/licensing.module'
 | 
			
		||||
import { ReviewModule } from './review/review.module'
 | 
			
		||||
import { ReviewRouteComponent } from './routes/review-route/review-route.component'
 | 
			
		||||
import { StageModule } from './stage/stage.module'
 | 
			
		||||
import { EditorModule } from './editor/editor.module'
 | 
			
		||||
import { ViewerModule } from './viewer/viewer.module'
 | 
			
		||||
import { ReviewModule } from './review/review.module'
 | 
			
		||||
import { DeployModule } from './deploy/deploy.module'
 | 
			
		||||
import { LicensingModule } from './licensing/licensing.module'
 | 
			
		||||
import { SystemModule } from './system/system.module'
 | 
			
		||||
import { ViewerModule } from './viewer/viewer.module'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Defining routes
 | 
			
		||||
@@ -45,7 +45,10 @@ export const ROUTES: Routes = [
 | 
			
		||||
    path: 'licensing',
 | 
			
		||||
    loadChildren: () => LicensingModule
 | 
			
		||||
  },
 | 
			
		||||
  { path: 'home', component: HomeComponent },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'home',
 | 
			
		||||
    loadChildren: () => HomeModule
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    /**
 | 
			
		||||
     * Load editor module with subroutes
 | 
			
		||||
 
 | 
			
		||||
@@ -112,7 +112,7 @@
 | 
			
		||||
 | 
			
		||||
                <div
 | 
			
		||||
                  *ngIf="
 | 
			
		||||
                    ['autocomplete'].includes(
 | 
			
		||||
                    ['autocomplete', 'autocomplete.custom'].includes(
 | 
			
		||||
                      $any(currentRecordValidator?.getRule(col.key)?.editor)
 | 
			
		||||
                    )
 | 
			
		||||
                  "
 | 
			
		||||
@@ -163,7 +163,7 @@
 | 
			
		||||
 | 
			
		||||
                <div
 | 
			
		||||
                  *ngIf="
 | 
			
		||||
                    ['autocomplete'].includes(
 | 
			
		||||
                    ['autocomplete', 'autocomplete.custom'].includes(
 | 
			
		||||
                      $any(currentRecordValidator?.getRule(col.key)?.editor)
 | 
			
		||||
                    )
 | 
			
		||||
                  "
 | 
			
		||||
 
 | 
			
		||||
@@ -186,24 +186,40 @@
 | 
			
		||||
            } as libdsParsed"
 | 
			
		||||
            class="editor-title text-center mt-0-i"
 | 
			
		||||
          >
 | 
			
		||||
            <clr-icon
 | 
			
		||||
              (click)="datasetInfo = true"
 | 
			
		||||
              shape="info-circle"
 | 
			
		||||
              class="is-highlight cursor-pointer"
 | 
			
		||||
              size="24"
 | 
			
		||||
            ></clr-icon>
 | 
			
		||||
            <clr-tooltip>
 | 
			
		||||
              <clr-icon
 | 
			
		||||
                clrTooltipTrigger
 | 
			
		||||
                (click)="datasetInfo = true"
 | 
			
		||||
                shape="info-circle"
 | 
			
		||||
                class="is-highlight cursor-pointer"
 | 
			
		||||
                size="24"
 | 
			
		||||
              ></clr-icon>
 | 
			
		||||
 | 
			
		||||
            <clr-icon
 | 
			
		||||
              *ngIf="libdsParsed.tableName.includes('-FC')"
 | 
			
		||||
              shape="bolt"
 | 
			
		||||
              class="color-yellow"
 | 
			
		||||
            ></clr-icon>
 | 
			
		||||
              <clr-icon
 | 
			
		||||
                *ngIf="libdsParsed.tableName.includes('-FC')"
 | 
			
		||||
                shape="bolt"
 | 
			
		||||
                class="color-yellow"
 | 
			
		||||
              ></clr-icon>
 | 
			
		||||
 | 
			
		||||
              <span clrTooltipTrigger>
 | 
			
		||||
                {{ libdsParsed.libName }}.<a
 | 
			
		||||
                  class="mr-10 view-table"
 | 
			
		||||
                  [routerLink]="'/view/data/' + libds!"
 | 
			
		||||
                  >{{ libdsParsed.tableName.replace('-FC', '') }}</a
 | 
			
		||||
                >
 | 
			
		||||
              </span>
 | 
			
		||||
 | 
			
		||||
              <ng-container *ngIf="this.dsNote && this.dsNote.length > 0">
 | 
			
		||||
                <clr-tooltip-content
 | 
			
		||||
                  clrPosition="bottom-left"
 | 
			
		||||
                  clrSize="lg"
 | 
			
		||||
                  *clrIfOpen
 | 
			
		||||
                >
 | 
			
		||||
                  {{ this.dsNote }}
 | 
			
		||||
                </clr-tooltip-content>
 | 
			
		||||
              </ng-container>
 | 
			
		||||
            </clr-tooltip>
 | 
			
		||||
 | 
			
		||||
            {{ libdsParsed.libName }}.<a
 | 
			
		||||
              class="mr-10"
 | 
			
		||||
              [routerLink]="'/view/data/' + libds!"
 | 
			
		||||
              >{{ libdsParsed.tableName.replace('-FC', '') }}</a
 | 
			
		||||
            >
 | 
			
		||||
            <ng-container *ngIf="dataSource">
 | 
			
		||||
              <ng-container *ngIf="!zeroFilterRows">
 | 
			
		||||
                ({{ dataSource.length | thousandSeparator: ',' }}
 | 
			
		||||
@@ -280,7 +296,7 @@
 | 
			
		||||
                    licenceState.value.editor_rows_allowed === 1
 | 
			
		||||
                      ? 'row'
 | 
			
		||||
                      : 'rows'
 | 
			
		||||
                  }}, contact support@datacontroller.io</span
 | 
			
		||||
                  }}, contact support@datacontroller.io</span
 | 
			
		||||
                >
 | 
			
		||||
              </clr-tooltip-content>
 | 
			
		||||
            </clr-tooltip>
 | 
			
		||||
@@ -417,7 +433,7 @@
 | 
			
		||||
                    licenceState.value.editor_rows_allowed === 1
 | 
			
		||||
                      ? 'row'
 | 
			
		||||
                      : 'rows'
 | 
			
		||||
                  }}, contact support@datacontroller.io</span
 | 
			
		||||
                  }}, contact support@datacontroller.io</span
 | 
			
		||||
                >
 | 
			
		||||
              </clr-tooltip-content>
 | 
			
		||||
            </clr-tooltip>
 | 
			
		||||
@@ -467,7 +483,7 @@
 | 
			
		||||
                        : 'rows'
 | 
			
		||||
                    }}
 | 
			
		||||
                    will be submitted. To remove the restriction, contact
 | 
			
		||||
                    support@datacontroller.io</span
 | 
			
		||||
                    support@datacontroller.io</span
 | 
			
		||||
                  >
 | 
			
		||||
                  <div *ngIf="tableTrue" class="clr-offset-md-2 clr-col-md-8">
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
@@ -528,7 +544,7 @@
 | 
			
		||||
                Due to current licence, only
 | 
			
		||||
                {{ licenceState.value.submit_rows_limit }} rows in a file will
 | 
			
		||||
                be submitted. To remove the restriction, contact
 | 
			
		||||
                support@datacontroller.io
 | 
			
		||||
                support@datacontroller.io
 | 
			
		||||
              </p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-footer">
 | 
			
		||||
@@ -830,6 +846,12 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</clr-modal>
 | 
			
		||||
 | 
			
		||||
<app-dataset-info [(open)]="datasetInfo" [dsmeta]="dsmeta"></app-dataset-info>
 | 
			
		||||
<app-dataset-info
 | 
			
		||||
  [(open)]="datasetInfo"
 | 
			
		||||
  [dsmeta]="dsmeta"
 | 
			
		||||
  [versions]="versions"
 | 
			
		||||
  (rowClicked)="datasetInfoModalRowClicked($event)"
 | 
			
		||||
>
 | 
			
		||||
</app-dataset-info>
 | 
			
		||||
 | 
			
		||||
<app-viewboxes [(viewboxModal)]="viewboxes"></app-viewboxes>
 | 
			
		||||
 
 | 
			
		||||
@@ -214,6 +214,10 @@ hot-table {
 | 
			
		||||
  width: 150px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.view-table {
 | 
			
		||||
  font-size: inherit !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FIXME
 | 
			
		||||
// Let's leave it here for a reference if there
 | 
			
		||||
// is an issue with viewboxes/filter modal overlaying
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,8 @@ import { HotTableInterface } from '../models/HotTable.interface'
 | 
			
		||||
import {
 | 
			
		||||
  $DataFormats,
 | 
			
		||||
  DSMeta,
 | 
			
		||||
  EditorsGetdataServiceResponse
 | 
			
		||||
  EditorsGetDataServiceResponse,
 | 
			
		||||
  Version
 | 
			
		||||
} from '../models/sas/editors-getdata.model'
 | 
			
		||||
import { DataFormat } from '../models/sas/common/DateFormat'
 | 
			
		||||
import SheetInfo from '../models/SheetInfo'
 | 
			
		||||
@@ -121,6 +122,8 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
 | 
			
		||||
  datasetInfo: boolean = false
 | 
			
		||||
  dsmeta: DSMeta[] = []
 | 
			
		||||
  versions: Version[] = []
 | 
			
		||||
  dsNote = ''
 | 
			
		||||
 | 
			
		||||
  viewboxes: boolean = false
 | 
			
		||||
 | 
			
		||||
@@ -939,13 +942,30 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
            return row.map((col: any, index: number) => {
 | 
			
		||||
              if (!col && col !== 0) col = ''
 | 
			
		||||
 | 
			
		||||
              if (isNaN(col)) {
 | 
			
		||||
                col = col.replace(/"/g, '""')
 | 
			
		||||
              /**
 | 
			
		||||
               * Keeping this for the reference
 | 
			
		||||
               * Code below used to convert JSON to CSV
 | 
			
		||||
               * now the XLSX is converting to CSV
 | 
			
		||||
               */
 | 
			
		||||
              // if (isNaN(col)) {
 | 
			
		||||
              //   // Match and replace the double quotes, ignore the first and last char
 | 
			
		||||
              //   // in case they are double quotes already
 | 
			
		||||
              //   col = col.replace(/(?<!^)"(?!$)/g, '""')
 | 
			
		||||
 | 
			
		||||
                if (col.search(/,/g) > -1) {
 | 
			
		||||
                  col = '"' + col + '"'
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              //   if (col.search(/,/g) > -1 ||
 | 
			
		||||
              //       col.search(/\r|\n/g) > -1
 | 
			
		||||
              //   ) {
 | 
			
		||||
              //     // Missing quotes at the end
 | 
			
		||||
              //     if (col.search(/"$/g) < 0) {
 | 
			
		||||
              //       col = col + '"' // So we add them
 | 
			
		||||
              //     }
 | 
			
		||||
 | 
			
		||||
              //     // Missing quotes at the start
 | 
			
		||||
              //     if (col.search(/^"/g) < 0) {
 | 
			
		||||
              //       col = '"' + col // So we add them
 | 
			
		||||
              //     }
 | 
			
		||||
              //   }
 | 
			
		||||
              // }
 | 
			
		||||
 | 
			
		||||
              const colName = this.headerShow[index]
 | 
			
		||||
              const colRule = this.dcValidator?.getRule(colName)
 | 
			
		||||
@@ -960,20 +980,30 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
 | 
			
		||||
          this.data = csvArrayData
 | 
			
		||||
 | 
			
		||||
          let csvContent = csvArrayHeaders.join(',') + '\n'
 | 
			
		||||
          // Apply licence rows limitation if exists
 | 
			
		||||
          csvContent += csvArrayData
 | 
			
		||||
            .slice(0, this.licenceState.value.submit_rows_limit)
 | 
			
		||||
            .map((e) => e.join(','))
 | 
			
		||||
            .join('\n')
 | 
			
		||||
          // Apply licence rows limitation if exists, it is only affecting data
 | 
			
		||||
          // which will be send to SAS
 | 
			
		||||
          const strippedCsvArrayData = csvArrayData.slice(
 | 
			
		||||
            0,
 | 
			
		||||
            this.licenceState.value.submit_rows_limit
 | 
			
		||||
          )
 | 
			
		||||
          // To submit to sas service, we need clean version of CSV of file
 | 
			
		||||
          // attached. XLSX will do the parsing and heavy lifting
 | 
			
		||||
          // First we create worksheet of json (data we extracted)
 | 
			
		||||
          let ws = XLSX.utils.json_to_sheet(strippedCsvArrayData, {
 | 
			
		||||
            skipHeader: true
 | 
			
		||||
          })
 | 
			
		||||
          // create CSV to be uploaded from worksheet
 | 
			
		||||
          let csvContentClean = XLSX.utils.sheet_to_csv(ws)
 | 
			
		||||
          // Prepend headers
 | 
			
		||||
          csvContentClean = csvArrayHeaders.join(',') + '\n' + csvContentClean
 | 
			
		||||
 | 
			
		||||
          if (this.encoding === 'WLATIN1') {
 | 
			
		||||
            let encoded = iconv.decode(Buffer.from(csvContent), 'CP-1252')
 | 
			
		||||
            let encoded = iconv.decode(Buffer.from(csvContentClean), 'CP-1252')
 | 
			
		||||
            let blob = new Blob([encoded], { type: 'application/csv' })
 | 
			
		||||
            let newCSVFile: File = this.blobToFile(blob, this.filename + '.csv')
 | 
			
		||||
            this.uploader.addToQueue([newCSVFile])
 | 
			
		||||
          } else {
 | 
			
		||||
            let blob = new Blob([csvContent], { type: 'application/csv' })
 | 
			
		||||
            let blob = new Blob([csvContentClean], { type: 'application/csv' })
 | 
			
		||||
            let newCSVFile: File = this.blobToFile(blob, this.filename + '.csv')
 | 
			
		||||
            this.uploader.addToQueue([newCSVFile])
 | 
			
		||||
          }
 | 
			
		||||
@@ -1928,13 +1958,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
 | 
			
		||||
        if (entry.values.length > 0) {
 | 
			
		||||
          hot.setCellMeta(entry.row, entry.col, 'renderer', 'autocomplete')
 | 
			
		||||
          hot.setCellMeta(entry.row, entry.col, 'editor', 'autocomplete')
 | 
			
		||||
          hot.setCellMeta(entry.row, entry.col, 'editor', 'autocomplete.custom')
 | 
			
		||||
          hot.setCellMeta(entry.row, entry.col, 'strict', entry.strict)
 | 
			
		||||
          hot.setCellMeta(entry.row, entry.col, 'filter', false)
 | 
			
		||||
 | 
			
		||||
          this.currentEditRecordValidator?.updateRule(entry.col, {
 | 
			
		||||
            renderer: 'autocomplete',
 | 
			
		||||
            editor: 'autocomplete',
 | 
			
		||||
            editor: 'autocomplete.custom',
 | 
			
		||||
            strict: entry.strict,
 | 
			
		||||
            filter: false
 | 
			
		||||
          })
 | 
			
		||||
@@ -2029,13 +2059,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      hot.setCellMeta(row, cellCol, 'renderer', 'autocomplete')
 | 
			
		||||
      hot.setCellMeta(row, cellCol, 'editor', 'autocomplete')
 | 
			
		||||
      hot.setCellMeta(row, cellCol, 'editor', 'autocomplete.custom')
 | 
			
		||||
      hot.setCellMeta(row, cellCol, 'strict', cellValidationEntry.strict)
 | 
			
		||||
      hot.setCellMeta(row, cellCol, 'filter', false)
 | 
			
		||||
 | 
			
		||||
      this.currentEditRecordValidator?.updateRule(cellCol, {
 | 
			
		||||
        renderer: 'autocomplete',
 | 
			
		||||
        editor: 'autocomplete',
 | 
			
		||||
        editor: 'autocomplete.custom',
 | 
			
		||||
        strict: cellValidationEntry.strict,
 | 
			
		||||
        filter: false
 | 
			
		||||
      })
 | 
			
		||||
@@ -2233,8 +2263,8 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        let txt: any = document.getElementById('formFields_8')
 | 
			
		||||
        txt.focus()
 | 
			
		||||
      })
 | 
			
		||||
        if (txt) txt.focus()
 | 
			
		||||
      }, 200)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // let cnt = 0;
 | 
			
		||||
@@ -2678,13 +2708,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
          const strict = this.cellValidationSource[validationSourceIndex].strict
 | 
			
		||||
 | 
			
		||||
          hot.setCellMeta(row, column, 'renderer', 'autocomplete')
 | 
			
		||||
          hot.setCellMeta(row, column, 'editor', 'autocomplete')
 | 
			
		||||
          hot.setCellMeta(row, column, 'editor', 'autocomplete.custom')
 | 
			
		||||
          hot.setCellMeta(row, column, 'strict', strict)
 | 
			
		||||
          hot.setCellMeta(row, column, 'filter', false)
 | 
			
		||||
 | 
			
		||||
          this.currentEditRecordValidator?.updateRule(column, {
 | 
			
		||||
            renderer: 'autocomplete',
 | 
			
		||||
            editor: 'autocomplete',
 | 
			
		||||
            editor: 'autocomplete.custom',
 | 
			
		||||
            strict: strict,
 | 
			
		||||
            filter: false
 | 
			
		||||
          })
 | 
			
		||||
@@ -2906,6 +2936,16 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  datasetInfoModalRowClicked(value: Version | DSMeta) {
 | 
			
		||||
    if ((<Version>value).LOAD_REF !== undefined) {
 | 
			
		||||
      // Type is Version
 | 
			
		||||
      const row = value as Version
 | 
			
		||||
      const url = `/stage/${row.LOAD_REF}`
 | 
			
		||||
 | 
			
		||||
      this.router.navigate([url])
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  viewboxManager() {
 | 
			
		||||
    this.viewboxes = true
 | 
			
		||||
  }
 | 
			
		||||
@@ -2918,6 +2958,37 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Function checks if selected hot cell is solo cell selected
 | 
			
		||||
   * and if it is, set the `filter` property based on filter param.
 | 
			
		||||
   *
 | 
			
		||||
   * @param filter
 | 
			
		||||
   */
 | 
			
		||||
  private setCellFilter(filter: boolean) {
 | 
			
		||||
    const hotSelected = this.hotInstance.getSelected()
 | 
			
		||||
    const selection = hotSelected ? hotSelected[0] : hotSelected
 | 
			
		||||
 | 
			
		||||
    // When we open a dropdown we want filter disabled so value in cell
 | 
			
		||||
    // don't filter out items, since we want to see them all.
 | 
			
		||||
    // But when we start typing we want to be able to start filtering values
 | 
			
		||||
    // again
 | 
			
		||||
    if (selection) {
 | 
			
		||||
      const startRow = selection[0]
 | 
			
		||||
      const endRow = selection[2]
 | 
			
		||||
      const startCell = selection[1]
 | 
			
		||||
      const endCell = selection[3]
 | 
			
		||||
 | 
			
		||||
      if (startRow === endRow && startCell === endCell) {
 | 
			
		||||
        const cellMeta = this.hotInstance.getCellMeta(startRow, startCell)
 | 
			
		||||
 | 
			
		||||
        // If filter is not already set at the value in the param, set it
 | 
			
		||||
        if (cellMeta && cellMeta.filter === !filter) {
 | 
			
		||||
          this.hotInstance.setCellMeta(startRow, startCell, 'filter', filter)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async ngOnInit() {
 | 
			
		||||
    this.licenceService.hot_license_key.subscribe(
 | 
			
		||||
      (hot_license_key: string | undefined) => {
 | 
			
		||||
@@ -2964,7 +3035,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
 | 
			
		||||
      await this.sasStoreService
 | 
			
		||||
        .callService(myParams, 'SASControlTable', 'editors/getdata', this.libds)
 | 
			
		||||
        .then((res: EditorsGetdataServiceResponse) => {
 | 
			
		||||
        .then((res: EditorsGetDataServiceResponse) => {
 | 
			
		||||
          this.initSetup(res)
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err: any) => {
 | 
			
		||||
@@ -2976,7 +3047,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit() {}
 | 
			
		||||
 | 
			
		||||
  initSetup(response: EditorsGetdataServiceResponse) {
 | 
			
		||||
  initSetup(response: EditorsGetDataServiceResponse) {
 | 
			
		||||
    this.hotInstance = this.hotRegisterer.getInstance('hotInstance')
 | 
			
		||||
 | 
			
		||||
    if (this.getdataError) return
 | 
			
		||||
@@ -2985,6 +3056,21 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
 | 
			
		||||
    this.cols = response.data.cols
 | 
			
		||||
    this.dsmeta = response.data.dsmeta
 | 
			
		||||
    this.versions = response.data.versions || []
 | 
			
		||||
 | 
			
		||||
    const notes = this.dsmeta.find((item) => item.NAME === 'NOTES')
 | 
			
		||||
    const longDesc = this.dsmeta.find((item) => item.NAME === 'DD_LONGDESC')
 | 
			
		||||
    const shortDesc = this.dsmeta.find((item) => item.NAME === 'DD_SHORTDESC')
 | 
			
		||||
 | 
			
		||||
    if (notes && notes.VALUE) {
 | 
			
		||||
      this.dsNote = notes.VALUE
 | 
			
		||||
    } else if (longDesc && longDesc.VALUE) {
 | 
			
		||||
      this.dsNote = longDesc.VALUE
 | 
			
		||||
    } else if (shortDesc && shortDesc.VALUE) {
 | 
			
		||||
      this.dsNote = shortDesc.VALUE
 | 
			
		||||
    } else {
 | 
			
		||||
      this.dsNote = ''
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const hot: Handsontable = this.hotInstance
 | 
			
		||||
 | 
			
		||||
@@ -3259,28 +3345,16 @@ export class EditorComponent implements OnInit, AfterViewInit {
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    hot.addHook('beforeKeyDown', (e: any) => {
 | 
			
		||||
      const hotSelected = this.hotInstance.getSelected()
 | 
			
		||||
      const selection = hotSelected ? hotSelected[0] : hotSelected
 | 
			
		||||
 | 
			
		||||
    hot.addHook('afterBeginEditing', () => {
 | 
			
		||||
      // When we open a dropdown we want filter disabled so value in cell
 | 
			
		||||
      // don't filter out items, since we want to see them all.
 | 
			
		||||
      this.setCellFilter(false)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    hot.addHook('beforeKeyDown', () => {
 | 
			
		||||
      // When we start typing, we are enabling the filter since we want to find
 | 
			
		||||
      // values faster.
 | 
			
		||||
      if (selection) {
 | 
			
		||||
        const startRow = selection[0]
 | 
			
		||||
        const endRow = selection[2]
 | 
			
		||||
        const startCell = selection[1]
 | 
			
		||||
        const endCell = selection[3]
 | 
			
		||||
 | 
			
		||||
        if (startRow === endRow && startCell === endCell) {
 | 
			
		||||
          const cellMeta = this.hotInstance.getCellMeta(startRow, startCell)
 | 
			
		||||
 | 
			
		||||
          if (cellMeta && cellMeta.filter === false) {
 | 
			
		||||
            this.hotInstance.setCellMeta(startRow, startCell, 'filter', true)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      this.setCellFilter(true)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    hot.addHook('afterChange', (source: any, change: any) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ import { EditRecordComponent } from './components/edit-record/edit-record.compon
 | 
			
		||||
import { UploadStaterComponent } from './components/upload-stater/upload-stater.component'
 | 
			
		||||
import { EditorRoutingModule } from './editor-routing.module'
 | 
			
		||||
import { EditorComponent } from './editor.component'
 | 
			
		||||
import { HomeModule } from '../home/home.module'
 | 
			
		||||
import { DcTreeModule } from '../shared/dc-tree/dc-tree.module'
 | 
			
		||||
import { DragDropModule } from '@angular/cdk/drag-drop'
 | 
			
		||||
import { ViewboxesModule } from '../shared/viewboxes/viewboxes.module'
 | 
			
		||||
@@ -33,7 +32,6 @@ registerAllModules()
 | 
			
		||||
    AppSharedModule,
 | 
			
		||||
    DirectivesModule,
 | 
			
		||||
    SharedModule,
 | 
			
		||||
    HomeModule,
 | 
			
		||||
    PipesModule,
 | 
			
		||||
    DcTreeModule,
 | 
			
		||||
    DragDropModule,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								client/src/app/home/home-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								client/src/app/home/home-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import { NgModule } from '@angular/core'
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router'
 | 
			
		||||
import { HomeRouteComponent } from '../routes/home-route/home-route.component'
 | 
			
		||||
import { HomeComponent } from './home.component'
 | 
			
		||||
import { XLMapModule } from '../xlmap/xlmap.module'
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: '',
 | 
			
		||||
    component: HomeRouteComponent,
 | 
			
		||||
    children: [
 | 
			
		||||
      { path: '', pathMatch: 'full', redirectTo: 'tables' },
 | 
			
		||||
      { path: 'tables', component: HomeComponent },
 | 
			
		||||
      { path: 'files', loadChildren: () => XLMapModule }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  imports: [RouterModule.forChild(routes)],
 | 
			
		||||
  exports: [RouterModule]
 | 
			
		||||
})
 | 
			
		||||
export class HomeRoutingModule {}
 | 
			
		||||
@@ -94,15 +94,17 @@
 | 
			
		||||
              {{ libTable.replace('-FC', '') }}
 | 
			
		||||
            </button>
 | 
			
		||||
 | 
			
		||||
            <clr-tooltip-content
 | 
			
		||||
              clrPosition="bottom-right"
 | 
			
		||||
              clrSize="lg"
 | 
			
		||||
              *clrIfOpen
 | 
			
		||||
            >
 | 
			
		||||
              <span *ngIf="tableLocked">
 | 
			
		||||
                To unlock all tables, contact support@datacontroller.io
 | 
			
		||||
              </span>
 | 
			
		||||
            </clr-tooltip-content>
 | 
			
		||||
            <ng-container *ngIf="tableLocked">
 | 
			
		||||
              <clr-tooltip-content
 | 
			
		||||
                clrPosition="bottom-right"
 | 
			
		||||
                clrSize="lg"
 | 
			
		||||
                *clrIfOpen
 | 
			
		||||
              >
 | 
			
		||||
                <span>
 | 
			
		||||
                  To unlock all tables, contact support@datacontroller.io
 | 
			
		||||
                </span>
 | 
			
		||||
              </clr-tooltip-content>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </clr-tooltip>
 | 
			
		||||
        </clr-tree-node>
 | 
			
		||||
      </clr-tree-node>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,18 @@
 | 
			
		||||
import { NgModule } from '@angular/core'
 | 
			
		||||
import { CommonModule } from '@angular/common'
 | 
			
		||||
import { HomeComponent } from './home.component'
 | 
			
		||||
import { ClarityModule } from '@clr/angular'
 | 
			
		||||
import { NgModule } from '@angular/core'
 | 
			
		||||
import { FormsModule } from '@angular/forms'
 | 
			
		||||
import { ClarityModule } from '@clr/angular'
 | 
			
		||||
import { AppSharedModule } from '../app-shared.module'
 | 
			
		||||
import { DcTreeModule } from '../shared/dc-tree/dc-tree.module'
 | 
			
		||||
import { DirectivesModule } from '../directives/directives.module'
 | 
			
		||||
import { HomeRouteComponent } from '../routes/home-route/home-route.component'
 | 
			
		||||
import { DcTreeModule } from '../shared/dc-tree/dc-tree.module'
 | 
			
		||||
import { HomeRoutingModule } from './home-routing.module'
 | 
			
		||||
import { HomeComponent } from './home.component'
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [HomeComponent],
 | 
			
		||||
  declarations: [HomeComponent, HomeRouteComponent],
 | 
			
		||||
  imports: [
 | 
			
		||||
    HomeRoutingModule,
 | 
			
		||||
    FormsModule,
 | 
			
		||||
    ClarityModule,
 | 
			
		||||
    AppSharedModule,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,147 +2,152 @@
 | 
			
		||||
  <div class="card-header">Licencing</div>
 | 
			
		||||
 | 
			
		||||
  <div [ngSwitch]="action" class="card-block">
 | 
			
		||||
    <ng-container *ngSwitchCase="'key'">
 | 
			
		||||
      <p class="key-error" *ngIf="!keyError">
 | 
			
		||||
        Licence key is invalid. We can't provide you more details at the moment
 | 
			
		||||
    <div class="card-text">
 | 
			
		||||
      <ng-container *ngSwitchCase="'key'">
 | 
			
		||||
        <p class="key-error" *ngIf="!keyError">
 | 
			
		||||
          Licence key is invalid. We can't provide you more details at the
 | 
			
		||||
          moment
 | 
			
		||||
        </p>
 | 
			
		||||
 | 
			
		||||
        <p
 | 
			
		||||
          class="key-error"
 | 
			
		||||
          *ngIf="keyError"
 | 
			
		||||
          [innerHTML]="licenseErrors[keyError]"
 | 
			
		||||
        ></p>
 | 
			
		||||
 | 
			
		||||
        <p *ngIf="errorDetails"><strong>Details:</strong> {{ errorDetails }}</p>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
 | 
			
		||||
      <ng-container *ngSwitchCase="'limit'">
 | 
			
		||||
        <p class="key-error">
 | 
			
		||||
          The registered number of users reached the limit specified for your
 | 
			
		||||
          licence. Please contact
 | 
			
		||||
          <contact-link classes="color-green" />
 | 
			
		||||
          or your reseller to arrange additional licences for this product.
 | 
			
		||||
        </p>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
 | 
			
		||||
      <ng-container *ngSwitchCase="'update'">
 | 
			
		||||
        <p class="key-error">
 | 
			
		||||
          Update the license key by uploading the licence file or by pasting a
 | 
			
		||||
          license key and activation key in the inputs below.
 | 
			
		||||
        </p>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
 | 
			
		||||
      <p>
 | 
			
		||||
        <strong>SYSSITE:</strong>
 | 
			
		||||
        <span
 | 
			
		||||
          *ngFor="let id of syssite.value; let i = index"
 | 
			
		||||
          [class.misskey]="missmatchedKey && missmatchedKey === id"
 | 
			
		||||
        >
 | 
			
		||||
          {{ id }}{{ i === syssite.value?.length! - 1 ? '' : ',' }}
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <a
 | 
			
		||||
          class="tooltip tooltip-md tooltip-top-right"
 | 
			
		||||
          (click)="copySyssite(copyIcon, copyTooltip, syssite.value || [])"
 | 
			
		||||
        >
 | 
			
		||||
          <clr-icon
 | 
			
		||||
            #copyIcon
 | 
			
		||||
            class="cursor-pointer"
 | 
			
		||||
            shape="copy"
 | 
			
		||||
            size="15"
 | 
			
		||||
          ></clr-icon>
 | 
			
		||||
          <span #copyTooltip class="tooltip-content">Copy to clipboard</span>
 | 
			
		||||
        </a>
 | 
			
		||||
      </p>
 | 
			
		||||
 | 
			
		||||
      <p
 | 
			
		||||
        class="key-error"
 | 
			
		||||
        *ngIf="keyError"
 | 
			
		||||
        [innerHTML]="licenseErrors[keyError]"
 | 
			
		||||
      ></p>
 | 
			
		||||
 | 
			
		||||
      <p *ngIf="errorDetails"><strong>Details:</strong> {{ errorDetails }}</p>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <ng-container *ngSwitchCase="'limit'">
 | 
			
		||||
      <p class="key-error">
 | 
			
		||||
        The registered number of users reached the limit specified for your
 | 
			
		||||
        licence. Please contact
 | 
			
		||||
        <contact-link classes="color-green" />
 | 
			
		||||
        or your reseller to arrange additional licences for this product.
 | 
			
		||||
      <p *ngIf="licenseKeyData && userCountLimitation" class="m-0">
 | 
			
		||||
        <strong>Allowed users:</strong>
 | 
			
		||||
        {{ licenseKeyData.users_allowed }}
 | 
			
		||||
      </p>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <ng-container *ngSwitchCase="'update'">
 | 
			
		||||
      <p class="key-error">
 | 
			
		||||
        Update the license key by uploading the licence file or by pasting a
 | 
			
		||||
        license key and activation key in the inputs below.
 | 
			
		||||
      </p>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <p>
 | 
			
		||||
      <strong>SYSSITE:</strong>
 | 
			
		||||
      <span
 | 
			
		||||
        *ngFor="let id of syssite.value; let i = index"
 | 
			
		||||
        [class.misskey]="missmatchedKey && missmatchedKey === id"
 | 
			
		||||
      >
 | 
			
		||||
        {{ id }}{{ i === syssite.value?.length! - 1 ? '' : ',' }}
 | 
			
		||||
      </span>
 | 
			
		||||
 | 
			
		||||
      <a
 | 
			
		||||
        class="tooltip tooltip-md tooltip-top-right"
 | 
			
		||||
        (click)="copySyssite(copyIcon, copyTooltip, syssite.value || [])"
 | 
			
		||||
      >
 | 
			
		||||
        <clr-icon
 | 
			
		||||
          #copyIcon
 | 
			
		||||
          class="cursor-pointer"
 | 
			
		||||
          shape="copy"
 | 
			
		||||
          size="15"
 | 
			
		||||
        ></clr-icon>
 | 
			
		||||
        <span #copyTooltip class="tooltip-content">Copy to clipboard</span>
 | 
			
		||||
      </a>
 | 
			
		||||
    </p>
 | 
			
		||||
 | 
			
		||||
    <p *ngIf="licenseKeyData && userCountLimitation" class="m-0">
 | 
			
		||||
      <strong>Allowed users:</strong>
 | 
			
		||||
      {{ licenseKeyData.users_allowed }}
 | 
			
		||||
    </p>
 | 
			
		||||
 | 
			
		||||
    <clr-tabs>
 | 
			
		||||
      <clr-tab>
 | 
			
		||||
        <button clrTabLink>Upload licence</button>
 | 
			
		||||
        <clr-tab-content>
 | 
			
		||||
          <input
 | 
			
		||||
            #licenceFile
 | 
			
		||||
            (change)="onFileCapture($event)"
 | 
			
		||||
            type="file"
 | 
			
		||||
            hidden
 | 
			
		||||
          />
 | 
			
		||||
          <div
 | 
			
		||||
            (click)="licenceFile.click()"
 | 
			
		||||
            appFileDrop
 | 
			
		||||
            (fileDrop)="onFileCapture($event, true)"
 | 
			
		||||
            class="drop-area"
 | 
			
		||||
          >
 | 
			
		||||
            <clr-spinner
 | 
			
		||||
              class="spinner-sm"
 | 
			
		||||
              *ngIf="licenceFileLoading"
 | 
			
		||||
            ></clr-spinner>
 | 
			
		||||
            <ng-container *ngIf="!licenceFileLoading">
 | 
			
		||||
              <div *ngIf="licencefile.filename === ''">
 | 
			
		||||
                Drop / Browse licence file
 | 
			
		||||
              </div>
 | 
			
		||||
              <div *ngIf="licencefile.filename !== ''">
 | 
			
		||||
                Selected file: <strong>{{ licencefile.filename }}</strong>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div *ngIf="licenceFileError">
 | 
			
		||||
                <strong>{{ licenceFileError }}</strong>
 | 
			
		||||
              </div>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </div>
 | 
			
		||||
        </clr-tab-content>
 | 
			
		||||
      </clr-tab>
 | 
			
		||||
 | 
			
		||||
      <clr-tab>
 | 
			
		||||
        <button clrTabLink>Paste licence</button>
 | 
			
		||||
        <clr-tab-content>
 | 
			
		||||
          <form class="clr-form license-key-form">
 | 
			
		||||
            <p>Licence key:</p>
 | 
			
		||||
            <div class="clr-control-container">
 | 
			
		||||
              <textarea
 | 
			
		||||
                [(ngModel)]="licenceKeyValue"
 | 
			
		||||
                (mouseleave)="trimKeys()"
 | 
			
		||||
                name="license-key-area"
 | 
			
		||||
                placeholder="Paste licence key here"
 | 
			
		||||
                class="clr-textarea"
 | 
			
		||||
              ></textarea>
 | 
			
		||||
      <clr-tabs>
 | 
			
		||||
        <clr-tab>
 | 
			
		||||
          <button clrTabLink>Upload licence</button>
 | 
			
		||||
          <clr-tab-content>
 | 
			
		||||
            <input
 | 
			
		||||
              #licenceFile
 | 
			
		||||
              (change)="onFileCapture($event)"
 | 
			
		||||
              type="file"
 | 
			
		||||
              hidden
 | 
			
		||||
            />
 | 
			
		||||
            <div
 | 
			
		||||
              (click)="licenceFile.click()"
 | 
			
		||||
              appFileDrop
 | 
			
		||||
              (fileDrop)="onFileCapture($event, true)"
 | 
			
		||||
              class="drop-area"
 | 
			
		||||
            >
 | 
			
		||||
              <clr-spinner
 | 
			
		||||
                class="spinner-sm"
 | 
			
		||||
                *ngIf="licenceFileLoading"
 | 
			
		||||
              ></clr-spinner>
 | 
			
		||||
              <ng-container *ngIf="!licenceFileLoading">
 | 
			
		||||
                <div *ngIf="licencefile.filename === ''">
 | 
			
		||||
                  Drop / Browse licence file
 | 
			
		||||
                </div>
 | 
			
		||||
                <div *ngIf="licencefile.filename !== ''">
 | 
			
		||||
                  Selected file: <strong>{{ licencefile.filename }}</strong>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div *ngIf="licenceFileError">
 | 
			
		||||
                  <strong>{{ licenceFileError }}</strong>
 | 
			
		||||
                </div>
 | 
			
		||||
              </ng-container>
 | 
			
		||||
            </div>
 | 
			
		||||
          </form>
 | 
			
		||||
          </clr-tab-content>
 | 
			
		||||
        </clr-tab>
 | 
			
		||||
 | 
			
		||||
          <form class="clr-form activation-key-form">
 | 
			
		||||
            <p>Activation key:</p>
 | 
			
		||||
            <div class="clr-control-container">
 | 
			
		||||
              <textarea
 | 
			
		||||
                [(ngModel)]="activationKeyValue"
 | 
			
		||||
                (mouseleave)="trimKeys()"
 | 
			
		||||
                name="activation-key-area"
 | 
			
		||||
                placeholder="Paste activation key here"
 | 
			
		||||
                class="clr-textarea"
 | 
			
		||||
              ></textarea>
 | 
			
		||||
            </div>
 | 
			
		||||
          </form>
 | 
			
		||||
        </clr-tab-content>
 | 
			
		||||
      </clr-tab>
 | 
			
		||||
    </clr-tabs>
 | 
			
		||||
        <clr-tab>
 | 
			
		||||
          <button clrTabLink>Paste licence</button>
 | 
			
		||||
          <clr-tab-content>
 | 
			
		||||
            <form class="clr-form license-key-form">
 | 
			
		||||
              <p>Licence key:</p>
 | 
			
		||||
              <div class="clr-control-container">
 | 
			
		||||
                <textarea
 | 
			
		||||
                  [(ngModel)]="licenceKeyValue"
 | 
			
		||||
                  (mouseleave)="trimKeys()"
 | 
			
		||||
                  name="license-key-area"
 | 
			
		||||
                  placeholder="Paste licence key here"
 | 
			
		||||
                  class="clr-textarea"
 | 
			
		||||
                ></textarea>
 | 
			
		||||
              </div>
 | 
			
		||||
            </form>
 | 
			
		||||
 | 
			
		||||
    <button
 | 
			
		||||
      (click)="applyKeys()"
 | 
			
		||||
      class="btn btn-primary apply-keys"
 | 
			
		||||
      [clrLoading]="applyingKeys"
 | 
			
		||||
      [disabled]="disableApplyButton"
 | 
			
		||||
    >
 | 
			
		||||
      Apply licence keys
 | 
			
		||||
    </button>
 | 
			
		||||
            <form class="clr-form activation-key-form">
 | 
			
		||||
              <p>Activation key:</p>
 | 
			
		||||
              <div class="clr-control-container">
 | 
			
		||||
                <textarea
 | 
			
		||||
                  [(ngModel)]="activationKeyValue"
 | 
			
		||||
                  (mouseleave)="trimKeys()"
 | 
			
		||||
                  name="activation-key-area"
 | 
			
		||||
                  placeholder="Paste activation key here"
 | 
			
		||||
                  class="clr-textarea"
 | 
			
		||||
                ></textarea>
 | 
			
		||||
              </div>
 | 
			
		||||
            </form>
 | 
			
		||||
          </clr-tab-content>
 | 
			
		||||
        </clr-tab>
 | 
			
		||||
      </clr-tabs>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <button
 | 
			
		||||
      *ngIf="isAppFreeTier.value"
 | 
			
		||||
      routerLink="/"
 | 
			
		||||
      class="btn btn-sm btn-link"
 | 
			
		||||
    >
 | 
			
		||||
      Continue with free tier
 | 
			
		||||
    </button>
 | 
			
		||||
    <div class="card-footer d-flex clr-align-items-center">
 | 
			
		||||
      <button
 | 
			
		||||
        (click)="applyKeys()"
 | 
			
		||||
        class="btn btn-primary apply-keys"
 | 
			
		||||
        [clrLoading]="applyingKeys"
 | 
			
		||||
        [disabled]="disableApplyButton"
 | 
			
		||||
      >
 | 
			
		||||
        Apply licence keys
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
      <button
 | 
			
		||||
        *ngIf="isAppFreeTier.value"
 | 
			
		||||
        routerLink="/"
 | 
			
		||||
        class="btn btn-sm btn-link"
 | 
			
		||||
      >
 | 
			
		||||
        Continue with free tier
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,6 @@
 | 
			
		||||
 | 
			
		||||
.apply-keys {
 | 
			
		||||
  height: 40px;
 | 
			
		||||
  width: 200px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.drop-area {
 | 
			
		||||
 
 | 
			
		||||
@@ -656,11 +656,10 @@ export class LineageComponent {
 | 
			
		||||
          this.flatdata = res.flatdata
 | 
			
		||||
 | 
			
		||||
          if (this.libraryList) {
 | 
			
		||||
            let libraryToSelect = this.libraryList.find(
 | 
			
		||||
              (library: any) =>
 | 
			
		||||
                res.info[0]?.LIBURI?.toUpperCase()?.includes(
 | 
			
		||||
                  library?.LIBRARYID?.toUpperCase()
 | 
			
		||||
                )
 | 
			
		||||
            let libraryToSelect = this.libraryList.find((library: any) =>
 | 
			
		||||
              res.info[0]?.LIBURI?.toUpperCase()?.includes(
 | 
			
		||||
                library?.LIBRARYID?.toUpperCase()
 | 
			
		||||
              )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            let tableToSelect: any
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ export interface FilterClause {
 | 
			
		||||
  operators: string[]
 | 
			
		||||
  type: string
 | 
			
		||||
  value: any
 | 
			
		||||
  valueVariable: boolean
 | 
			
		||||
  values: { formatted: string; unformatted: any }[]
 | 
			
		||||
  variable: string
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,12 +4,12 @@ import { DQData, SASParam } from '../TableData'
 | 
			
		||||
import { BaseSASResponse } from './common/BaseSASResponse'
 | 
			
		||||
import { DataFormat } from './common/DateFormat'
 | 
			
		||||
 | 
			
		||||
export interface EditorsGetdataServiceResponse {
 | 
			
		||||
  data: EditorsGetdataSASResponse
 | 
			
		||||
export interface EditorsGetDataServiceResponse {
 | 
			
		||||
  data: EditorsGetDataSASResponse
 | 
			
		||||
  libds: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EditorsGetdataSASResponse extends BaseSASResponse {
 | 
			
		||||
export interface EditorsGetDataSASResponse extends BaseSASResponse {
 | 
			
		||||
  $sasdata: $DataFormats
 | 
			
		||||
  sasdata: Sasdata[]
 | 
			
		||||
  sasparams: SASParam[]
 | 
			
		||||
@@ -17,6 +17,7 @@ export interface EditorsGetdataSASResponse extends BaseSASResponse {
 | 
			
		||||
  dqrules: DQRule[]
 | 
			
		||||
  dsmeta: DSMeta[]
 | 
			
		||||
  dqdata: DQData[]
 | 
			
		||||
  versions: Version[]
 | 
			
		||||
  cols: Col[]
 | 
			
		||||
  maxvarlengths: Maxvarlength[]
 | 
			
		||||
  xl_rules: any[]
 | 
			
		||||
@@ -27,6 +28,18 @@ export interface DSMeta {
 | 
			
		||||
  ODS_TABLE: string
 | 
			
		||||
  NAME: string
 | 
			
		||||
  VALUE: string
 | 
			
		||||
  [key: string]: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Version {
 | 
			
		||||
  LOAD_REF: string
 | 
			
		||||
  USER_NM: string
 | 
			
		||||
  VERSION_DTTM: string
 | 
			
		||||
  VERSION_DESC: string
 | 
			
		||||
  CHANGED_RECORDS: number
 | 
			
		||||
  NEW_RECORDS: number
 | 
			
		||||
  DELETED_RECORDS: number
 | 
			
		||||
  [key: string]: string | number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Sasdata {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								client/src/app/models/sas/editors-restore.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								client/src/app/models/sas/editors-restore.model.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
import { BaseSASResponse } from './common/BaseSASResponse'
 | 
			
		||||
 | 
			
		||||
export interface EditorsRestoreServiceResponse extends BaseSASResponse {
 | 
			
		||||
  restore_out: RestoreOut[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RestoreOut {
 | 
			
		||||
  LOADREF: string
 | 
			
		||||
}
 | 
			
		||||
@@ -143,7 +143,6 @@
 | 
			
		||||
                  (ngModelChange)="
 | 
			
		||||
                    setVariableOperator(queryIndex, query.operator, clauseIndex)
 | 
			
		||||
                  "
 | 
			
		||||
                  class="mt-2"
 | 
			
		||||
                  clrSelect
 | 
			
		||||
                >
 | 
			
		||||
                  <option *ngFor="let opr of query.operators">{{ opr }}</option>
 | 
			
		||||
@@ -413,7 +412,10 @@
 | 
			
		||||
>
 | 
			
		||||
  <app-soft-select
 | 
			
		||||
    label="Value"
 | 
			
		||||
    [secondLabel]="'Variable'"
 | 
			
		||||
    [emitOnlySelected]="query.valueVariable"
 | 
			
		||||
    [inputId]="'vals_' + queryIndex + '_' + clauseIndex"
 | 
			
		||||
    (selectedLabelChange)="selectedLabelChange($event, query)"
 | 
			
		||||
    [(value)]="query.value"
 | 
			
		||||
    [enableLoadMore]="query.nobs > query.values.length"
 | 
			
		||||
    (onInputEvent)="
 | 
			
		||||
@@ -423,9 +425,19 @@
 | 
			
		||||
      onAutocompleteLoadingMore($event, query.variable, queryIndex, clauseIndex)
 | 
			
		||||
    "
 | 
			
		||||
  >
 | 
			
		||||
    <option [value]="column.unformatted" *ngFor="let column of query.values">
 | 
			
		||||
      {{ column.formatted.trim() }}
 | 
			
		||||
    </option>
 | 
			
		||||
    <div *ngIf="!query.valueVariable">
 | 
			
		||||
      <option [value]="column.unformatted" *ngFor="let column of query.values">
 | 
			
		||||
        {{ column.formatted.trim() }}
 | 
			
		||||
      </option>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div *ngIf="query.valueVariable">
 | 
			
		||||
      <ng-container *ngFor="let column of cols">
 | 
			
		||||
        <option [value]="column.NAME" *ngIf="column.TYPE === query.type">
 | 
			
		||||
          {{ column.NAME }}
 | 
			
		||||
        </option>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
    </div>
 | 
			
		||||
  </app-soft-select>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -95,6 +95,7 @@ export class QueryComponent
 | 
			
		||||
        variable: null,
 | 
			
		||||
        operator: null,
 | 
			
		||||
        value: null,
 | 
			
		||||
        valueVariable: false,
 | 
			
		||||
        startrow: 0,
 | 
			
		||||
        rows: 0,
 | 
			
		||||
        nobs: 0,
 | 
			
		||||
@@ -193,6 +194,20 @@ export class QueryComponent
 | 
			
		||||
   */
 | 
			
		||||
  usePickersChange() {
 | 
			
		||||
    this.queryDateTime = []
 | 
			
		||||
    if (this.usePickers) {
 | 
			
		||||
      this.clauses.queryObj.forEach((queryObj: any) => {
 | 
			
		||||
        queryObj.elements.forEach((element: any) => {
 | 
			
		||||
          const isDateOrTime = ['DATETIME', 'TIME', 'DATE'].includes(
 | 
			
		||||
            element.ddtype
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
          if (isDateOrTime && element.valueVariable) {
 | 
			
		||||
            element.value = ''
 | 
			
		||||
            element.valueVariable = false
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -253,8 +268,6 @@ export class QueryComponent
 | 
			
		||||
      get(globals, objPath).filter.libds = this.libds
 | 
			
		||||
    }
 | 
			
		||||
    get(globals, objPath).filter.clauses = this.clauses
 | 
			
		||||
 | 
			
		||||
    console.log('globals', globals)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -750,6 +763,12 @@ export class QueryComponent
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public selectedLabelChange(label: string, query: any) {
 | 
			
		||||
    query.valueVariable = label === 'Variable'
 | 
			
		||||
    query.value = ''
 | 
			
		||||
    this.whereClauseFn()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public variableInputChange(
 | 
			
		||||
    queryVariable: any,
 | 
			
		||||
    index: number,
 | 
			
		||||
@@ -859,17 +878,25 @@ export class QueryComponent
 | 
			
		||||
   */
 | 
			
		||||
  public hasInvalidCluase(clauses: any): boolean {
 | 
			
		||||
    for (let clause of clauses) {
 | 
			
		||||
      clause['invalidClause'] = false
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
        clause.variable === null ||
 | 
			
		||||
        clause.operator === null ||
 | 
			
		||||
        clause.value === null ||
 | 
			
		||||
        clause.value === ''
 | 
			
		||||
        clause.value === '' &&
 | 
			
		||||
        !(clause.operator === 'NE' || clause.operator === 'CONTAINS')
 | 
			
		||||
      ) {
 | 
			
		||||
        clause['invalidClause'] = true
 | 
			
		||||
 | 
			
		||||
        return true
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
        clause.variable === null ||
 | 
			
		||||
        clause.operator === null ||
 | 
			
		||||
        clause.value === null
 | 
			
		||||
      ) {
 | 
			
		||||
        clause['invalidClause'] = true
 | 
			
		||||
 | 
			
		||||
        return true
 | 
			
		||||
      } else {
 | 
			
		||||
        clause['invalidClause'] = false
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -139,10 +139,7 @@
 | 
			
		||||
        <div class="card-header p-0">
 | 
			
		||||
          <div class="clr-row">
 | 
			
		||||
            <div class="clr-col-md-4 approvalBack">
 | 
			
		||||
              <span
 | 
			
		||||
                class="btn btn-sm btn-outline m-0"
 | 
			
		||||
                (click)="goToApprovalsList()"
 | 
			
		||||
              >
 | 
			
		||||
              <span class="btn btn-outline m-0" (click)="goToApprovalsList()">
 | 
			
		||||
                <clr-icon shape="caret" dir="left" size="20"></clr-icon>Back to
 | 
			
		||||
                approvals list
 | 
			
		||||
              </span>
 | 
			
		||||
@@ -209,22 +206,22 @@
 | 
			
		||||
                  <div class="d-flex justify-content-center mt-0">
 | 
			
		||||
                    <div class="clr-row clr-gap-5 clr-gap-sm-0">
 | 
			
		||||
                      <button
 | 
			
		||||
                        class="btn btn-sm btn-outline text-center mt-5"
 | 
			
		||||
                        class="btn btn-sm btn-outline text-center mt-5 mr-5i"
 | 
			
		||||
                        (click)="goToBase(jsParams?.TABLE_NM)"
 | 
			
		||||
                      >
 | 
			
		||||
                        Go to base table screen
 | 
			
		||||
                        View base table
 | 
			
		||||
                      </button>
 | 
			
		||||
                      <button
 | 
			
		||||
                        class="btn btn-sm btn-success-outline text-center mt-5"
 | 
			
		||||
                        class="btn btn-sm btn-success-outline text-center mt-5 mr-5i"
 | 
			
		||||
                        (click)="getTable(tableId)"
 | 
			
		||||
                      >
 | 
			
		||||
                        Go to edited screen
 | 
			
		||||
                        View staged data
 | 
			
		||||
                      </button>
 | 
			
		||||
                      <button
 | 
			
		||||
                        class="btn btn-sm btn-info-outline text-center mt-5"
 | 
			
		||||
                        (click)="goBack(jsParams?.TABLE_NM)"
 | 
			
		||||
                      >
 | 
			
		||||
                        Go back to editor
 | 
			
		||||
                        Edit base table
 | 
			
		||||
                      </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
@@ -236,7 +233,7 @@
 | 
			
		||||
                    id="acceptBtn"
 | 
			
		||||
                    [clrLoading]="acceptLoading"
 | 
			
		||||
                    type="submit"
 | 
			
		||||
                    class="btn btn-sm btn-success"
 | 
			
		||||
                    class="btn btn-sm btn-success mr-5i"
 | 
			
		||||
                    (click)="approveTable()"
 | 
			
		||||
                    [disabled]="
 | 
			
		||||
                      !loadingTable || params?.ISAPPROVER === 'NO' || noChanges
 | 
			
		||||
@@ -246,7 +243,7 @@
 | 
			
		||||
                  </button>
 | 
			
		||||
                  <button
 | 
			
		||||
                    id="rejectBtn"
 | 
			
		||||
                    class="btn btn-sm btn btn-danger mr-0"
 | 
			
		||||
                    class="btn btn-sm btn btn-danger mr-5i"
 | 
			
		||||
                    (click)="rejectOpen = true"
 | 
			
		||||
                    [disabled]="
 | 
			
		||||
                      !loadingTable || params?.ISAPPROVER === 'NO' || noChanges
 | 
			
		||||
@@ -394,9 +391,9 @@
 | 
			
		||||
        <div class="card-header">
 | 
			
		||||
          <div class="clr-row">
 | 
			
		||||
            <div class="clr-col-md-4 approvalBack">
 | 
			
		||||
              <span class="btn btn-sm btn-outline" (click)="goToSubmitList()">
 | 
			
		||||
                <clr-icon shape="caret" dir="left" size="20"></clr-icon>Back to
 | 
			
		||||
                submitted list
 | 
			
		||||
              <span class="btn btn-outline" (click)="goToSubmitList()">
 | 
			
		||||
                <cds-icon shape="angle" direction="left" size="20"></cds-icon
 | 
			
		||||
                >Back to submitted list
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="clr-col-md-4">
 | 
			
		||||
@@ -443,22 +440,22 @@
 | 
			
		||||
              <div class="d-flex justify-content-center mt-0">
 | 
			
		||||
                <div class="clr-row clr-gap-5 clr-gap-sm-0">
 | 
			
		||||
                  <button
 | 
			
		||||
                    class="btn btn-sm btn-outline text-center mt-5"
 | 
			
		||||
                    class="btn btn-sm btn-outline text-center mt-5 mr-5i"
 | 
			
		||||
                    (click)="goToBase(subObj.base)"
 | 
			
		||||
                  >
 | 
			
		||||
                    Go to base table screen
 | 
			
		||||
                    View base table
 | 
			
		||||
                  </button>
 | 
			
		||||
                  <button
 | 
			
		||||
                    class="btn btn-sm btn-success-outline text-center mt-5"
 | 
			
		||||
                    class="btn btn-sm btn-success-outline text-center mt-5 mr-5i"
 | 
			
		||||
                    (click)="getTable(subObj.tableId)"
 | 
			
		||||
                  >
 | 
			
		||||
                    Go to edited screen
 | 
			
		||||
                    View staged data
 | 
			
		||||
                  </button>
 | 
			
		||||
                  <button
 | 
			
		||||
                    class="btn btn-sm btn-info-outline text-center mt-5"
 | 
			
		||||
                    (click)="goBack(subObj.base)"
 | 
			
		||||
                  >
 | 
			
		||||
                    Go back to editor
 | 
			
		||||
                    Edit base table
 | 
			
		||||
                  </button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -72,7 +72,7 @@
 | 
			
		||||
      >
 | 
			
		||||
        To unlock more than
 | 
			
		||||
        {{ licenceState.value.history_rows_allowed }} records, contact
 | 
			
		||||
        support@datacontroller.io
 | 
			
		||||
        support@datacontroller.io
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
<router-outlet></router-outlet>
 | 
			
		||||
							
								
								
									
										17
									
								
								client/src/app/routes/home-route/home-route.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								client/src/app/routes/home-route/home-route.component.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy } from '@angular/core'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-home-route',
 | 
			
		||||
  templateUrl: './home-route.component.html',
 | 
			
		||||
  styleUrls: ['./home-route.component.scss'],
 | 
			
		||||
  host: {
 | 
			
		||||
    class: 'content-container'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
export class HomeRouteComponent implements OnInit, OnDestroy {
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {}
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
<router-outlet></router-outlet>
 | 
			
		||||
							
								
								
									
										17
									
								
								client/src/app/routes/xlmap-route/xlmap-route.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								client/src/app/routes/xlmap-route/xlmap-route.component.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy } from '@angular/core'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-xlmap-route',
 | 
			
		||||
  templateUrl: './xlmap-route.component.html',
 | 
			
		||||
  styleUrls: ['./xlmap-route.component.scss'],
 | 
			
		||||
  host: {
 | 
			
		||||
    class: 'content-container'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
export class XLMapRouteComponent implements OnInit, OnDestroy {
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {}
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {}
 | 
			
		||||
}
 | 
			
		||||
@@ -74,6 +74,7 @@ export class AppService {
 | 
			
		||||
          missingProps.push('Globvars')
 | 
			
		||||
        if (!res.sasdatasets) missingProps.push('Sasdatasets')
 | 
			
		||||
        if (!res.saslibs) missingProps.push('Saslibs')
 | 
			
		||||
        if (!res.xlmaps) missingProps.push('XLMaps')
 | 
			
		||||
 | 
			
		||||
        if (missingProps.length > 0) {
 | 
			
		||||
          startupServiceError = true
 | 
			
		||||
@@ -135,10 +136,17 @@ export class AppService {
 | 
			
		||||
          globals.editor.libsAndTables = libsAndTables
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        globals.xlmaps = res.xlmaps.map((xlmap: any) => ({
 | 
			
		||||
          id: xlmap[0],
 | 
			
		||||
          description: xlmap[1],
 | 
			
		||||
          targetDS: xlmap[2]
 | 
			
		||||
        }))
 | 
			
		||||
        globals.editor.treeNodeLibraries = treeNodeLibraries
 | 
			
		||||
        globals.editor.libraries = libraries
 | 
			
		||||
        globals.editor.startupSet = true
 | 
			
		||||
 | 
			
		||||
        globals.dcLib = res.globvars[0].DCLIB
 | 
			
		||||
 | 
			
		||||
        await this.licenceService.activation(res)
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err: any) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,8 @@ import { globals } from '../_globals'
 | 
			
		||||
import { FilterClause, FilterGroup, FilterQuery } from '../models/FilterQuery'
 | 
			
		||||
import {
 | 
			
		||||
  $DataFormats,
 | 
			
		||||
  EditorsGetdataSASResponse,
 | 
			
		||||
  EditorsGetdataServiceResponse
 | 
			
		||||
  EditorsGetDataSASResponse,
 | 
			
		||||
  EditorsGetDataServiceResponse
 | 
			
		||||
} from '../models/sas/editors-getdata.model'
 | 
			
		||||
import { LoggerService } from './logger.service'
 | 
			
		||||
import { isSpecialMissing } from '@sasjs/utils/input/validators'
 | 
			
		||||
@@ -57,13 +57,13 @@ export class SasStoreService {
 | 
			
		||||
    libds: string
 | 
			
		||||
  ) {
 | 
			
		||||
    this.libds = libds
 | 
			
		||||
    let tables: any = {}
 | 
			
		||||
    const tables: any = {}
 | 
			
		||||
    tables[tableName] = [tableData]
 | 
			
		||||
    let res: EditorsGetdataSASResponse = await this.sasService.request(
 | 
			
		||||
    const res: EditorsGetDataSASResponse = await this.sasService.request(
 | 
			
		||||
      program,
 | 
			
		||||
      tables
 | 
			
		||||
    )
 | 
			
		||||
    let response: EditorsGetdataServiceResponse = {
 | 
			
		||||
    const response: EditorsGetDataServiceResponse = {
 | 
			
		||||
      data: res,
 | 
			
		||||
      libds: this.libds
 | 
			
		||||
    }
 | 
			
		||||
@@ -209,6 +209,14 @@ export class SasStoreService {
 | 
			
		||||
    return res
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getXLMapRules(id: string) {
 | 
			
		||||
    const tables = {
 | 
			
		||||
      getxlmaps_in: [{ XLMAP_ID: id }]
 | 
			
		||||
    }
 | 
			
		||||
    const res: any = await this.sasService.request('editors/getxlmaps', tables)
 | 
			
		||||
    return res
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getDetails(tableData: any, tableName: string, program: string) {
 | 
			
		||||
    let tables: any = {}
 | 
			
		||||
    tables[tableName] = [tableData]
 | 
			
		||||
@@ -408,14 +416,18 @@ export class SasStoreService {
 | 
			
		||||
    for (let index = 0; index < clauses.queryObj.length; index++) {
 | 
			
		||||
      let string = ''
 | 
			
		||||
      let clause = clauses.queryObj[index]
 | 
			
		||||
 | 
			
		||||
      for (let ind = 0; ind < clause.elements.length; ind++) {
 | 
			
		||||
        let query = clause.elements[ind]
 | 
			
		||||
 | 
			
		||||
        if (ind < clause.elements.length - 1) {
 | 
			
		||||
          opr = clause.clauseLogic
 | 
			
		||||
        } else {
 | 
			
		||||
          opr = ''
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let val: any
 | 
			
		||||
 | 
			
		||||
        for (let k = 0; k < query.values.length; k++) {
 | 
			
		||||
          if (
 | 
			
		||||
            typeof query.value === 'string' &&
 | 
			
		||||
@@ -486,6 +498,8 @@ export class SasStoreService {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let type = query.type
 | 
			
		||||
        //if the value is variable, omit quotes in the 'where' string
 | 
			
		||||
        const isValueVariable = query.valueVariable
 | 
			
		||||
        let variable = query.variable === null ? '' : query.variable
 | 
			
		||||
        let oper = query.operator === null ? '' : query.operator
 | 
			
		||||
        // let value = val === null ? "''" : val;
 | 
			
		||||
@@ -499,10 +513,14 @@ export class SasStoreService {
 | 
			
		||||
        if (type === 'char' && oper !== 'IN' && oper !== 'NOT IN') {
 | 
			
		||||
          if (typeof value === 'undefined') {
 | 
			
		||||
            value = ''
 | 
			
		||||
            value = " '" + value + "' "
 | 
			
		||||
          } else {
 | 
			
		||||
            value = " '" + value + "' "
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (isValueVariable) {
 | 
			
		||||
            value = ' ' + value + ' ' //without quotes, with spaces
 | 
			
		||||
          } else {
 | 
			
		||||
            value = " '" + value + "' " //with quotes and spaces
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          string = string + ' ' + variable + ' ' + oper + value + opr
 | 
			
		||||
        } else {
 | 
			
		||||
          if (type === 'num' && typeof value === 'undefined') {
 | 
			
		||||
@@ -596,7 +614,7 @@ export class SasStoreService {
 | 
			
		||||
              rawValue = '.'
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            if (filterClause.type === 'char') {
 | 
			
		||||
            if (filterClause.type === 'char' && !filterClause.valueVariable) {
 | 
			
		||||
              rawValue = `'${filterClause.value.replace(/'/g, "''")}'`
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,5 +2,5 @@
 | 
			
		||||
  [ngClass]="classes"
 | 
			
		||||
  [class.unset]="classes !== ''"
 | 
			
		||||
  href="mailto:support@datacontroller.io?subject=Licence"
 | 
			
		||||
  >support@datacontroller.io</a
 | 
			
		||||
  >support@datacontroller.io</a
 | 
			
		||||
>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,25 +6,31 @@
 | 
			
		||||
>
 | 
			
		||||
  <h3 class="modal-title center text-center color-darker-gray">Dataset Meta</h3>
 | 
			
		||||
  <div class="modal-body">
 | 
			
		||||
    <p *ngIf="dsmetaGroupped.length < 1" class="text-center">
 | 
			
		||||
    <p *ngIf="dsmetaTabs.length < 1" class="text-center">
 | 
			
		||||
      No dataset meta to show.
 | 
			
		||||
    </p>
 | 
			
		||||
 | 
			
		||||
    <clr-tabs clrLayout="vertical">
 | 
			
		||||
      <clr-tab *ngFor="let dsmeta of dsmetaGroupped; let index = index">
 | 
			
		||||
        <button clrTabLink id="link1">{{ dsmeta.group }}</button>
 | 
			
		||||
      <clr-tab *ngFor="let tab of tabs; let index = index">
 | 
			
		||||
        <button clrTabLink id="link1">{{ tab.name }}</button>
 | 
			
		||||
        <clr-tab-content
 | 
			
		||||
          id="content1"
 | 
			
		||||
          *clrIfActive="index === 0"
 | 
			
		||||
          class="d-flex clr-justify-content-center w-100"
 | 
			
		||||
        >
 | 
			
		||||
          <clr-datagrid>
 | 
			
		||||
            <clr-dg-column>Name</clr-dg-column>
 | 
			
		||||
            <clr-dg-column>Value</clr-dg-column>
 | 
			
		||||
            <ng-container *ngFor="let col of tab.colsToDisplay">
 | 
			
		||||
              <clr-dg-column>{{ col.colName || col.colKey }}</clr-dg-column>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
 | 
			
		||||
            <clr-dg-row *ngFor="let info of dsmeta.dsmeta">
 | 
			
		||||
              <clr-dg-cell>{{ info.NAME }}</clr-dg-cell>
 | 
			
		||||
              <clr-dg-cell>{{ info.VALUE }}</clr-dg-cell>
 | 
			
		||||
            <clr-dg-row
 | 
			
		||||
              (click)="tab.onRowClick ? tab.onRowClick(info) : ''"
 | 
			
		||||
              class="clickable-row"
 | 
			
		||||
              *ngFor="let info of tab.meta"
 | 
			
		||||
            >
 | 
			
		||||
              <ng-container *ngFor="let col of tab.colsToDisplay">
 | 
			
		||||
                <clr-dg-cell>{{ info[col.colKey] }}</clr-dg-cell>
 | 
			
		||||
              </ng-container>
 | 
			
		||||
            </clr-dg-row>
 | 
			
		||||
          </clr-datagrid>
 | 
			
		||||
        </clr-tab-content>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,4 +13,23 @@
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
clr-modal {
 | 
			
		||||
  ::ng-deep {
 | 
			
		||||
    .modal-dialog {
 | 
			
		||||
      height: 100%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clickable-row {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::ng-deep {
 | 
			
		||||
  .datagrid-table .datagrid-cell:focus {
 | 
			
		||||
    outline: none;
 | 
			
		||||
    outline-offset: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -7,8 +7,8 @@ import {
 | 
			
		||||
  Output,
 | 
			
		||||
  SimpleChanges
 | 
			
		||||
} from '@angular/core'
 | 
			
		||||
import { DSMeta } from 'src/app/models/sas/editors-getdata.model'
 | 
			
		||||
import { DSMetaGroupped } from './models/dsmeta-groupped.model'
 | 
			
		||||
import { DSMeta, Version } from 'src/app/models/sas/editors-getdata.model'
 | 
			
		||||
import { Tab } from './models/dsmeta-groupped.model'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-dataset-info',
 | 
			
		||||
@@ -18,10 +18,15 @@ import { DSMetaGroupped } from './models/dsmeta-groupped.model'
 | 
			
		||||
export class DatasetInfoComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() open: boolean = false
 | 
			
		||||
  @Input() dsmeta: DSMeta[] = []
 | 
			
		||||
  @Input() versions: Version[] = []
 | 
			
		||||
 | 
			
		||||
  @Output() openChange = new EventEmitter<boolean>()
 | 
			
		||||
  @Output() rowClicked = new EventEmitter<Version | DSMeta>()
 | 
			
		||||
 | 
			
		||||
  dsmetaGroupped: DSMetaGroupped[] = []
 | 
			
		||||
  dsmetaTabs: Tab<DSMeta>[] = []
 | 
			
		||||
  versionsTabs: Tab<Version>[] = []
 | 
			
		||||
 | 
			
		||||
  tabs: Tab<DSMeta | Version>[] = []
 | 
			
		||||
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
@@ -30,28 +35,58 @@ export class DatasetInfoComponent implements OnInit, OnChanges {
 | 
			
		||||
  ngOnChanges(changes: SimpleChanges): void {
 | 
			
		||||
    if (changes.dsmeta?.currentValue?.length > 0) {
 | 
			
		||||
      this.parseDSMeta()
 | 
			
		||||
      this.parseVersions()
 | 
			
		||||
 | 
			
		||||
      this.tabs = [...[...this.dsmetaTabs], ...[...this.versionsTabs]]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  parseDSMeta() {
 | 
			
		||||
    this.dsmetaGroupped = []
 | 
			
		||||
    this.dsmetaTabs = []
 | 
			
		||||
 | 
			
		||||
    for (let info of this.dsmeta) {
 | 
			
		||||
      let groupIndex = this.dsmetaGroupped.findIndex(
 | 
			
		||||
        (x) => x.group === info.ODS_TABLE
 | 
			
		||||
      let groupIndex = this.dsmetaTabs.findIndex(
 | 
			
		||||
        (x) => x.name === info.ODS_TABLE
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      if (groupIndex < 0)
 | 
			
		||||
        groupIndex =
 | 
			
		||||
          this.dsmetaGroupped.push({
 | 
			
		||||
            group: info.ODS_TABLE,
 | 
			
		||||
            dsmeta: []
 | 
			
		||||
          this.dsmetaTabs.push({
 | 
			
		||||
            name: info.ODS_TABLE,
 | 
			
		||||
            title: 'Dataset Meta',
 | 
			
		||||
            colsToDisplay: [{ colKey: 'NAME' }, { colKey: 'VALUE' }],
 | 
			
		||||
            meta: [],
 | 
			
		||||
            onRowClick: (value: DSMeta) => {
 | 
			
		||||
              this.rowClicked.emit(value)
 | 
			
		||||
            }
 | 
			
		||||
          }) - 1
 | 
			
		||||
 | 
			
		||||
      this.dsmetaGroupped[groupIndex].dsmeta.push(info)
 | 
			
		||||
      this.dsmetaTabs[groupIndex].meta.push(info)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  parseVersions() {
 | 
			
		||||
    this.versionsTabs = [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'VERSIONS',
 | 
			
		||||
        title: 'Dataset Meta',
 | 
			
		||||
        colsToDisplay: [
 | 
			
		||||
          { colKey: 'LOAD_REF' },
 | 
			
		||||
          { colKey: 'USER_NM' },
 | 
			
		||||
          { colKey: 'VERSION_DTTM' },
 | 
			
		||||
          { colKey: 'NEW_RECORDS', colName: 'ADD' },
 | 
			
		||||
          { colKey: 'CHANGED_RECORDS', colName: 'MOD' },
 | 
			
		||||
          { colKey: 'DELETED_RECORDS', colName: 'DEL' },
 | 
			
		||||
          { colKey: 'VERSION_DESC' }
 | 
			
		||||
        ],
 | 
			
		||||
        meta: this.versions,
 | 
			
		||||
        onRowClick: (value: Version) => {
 | 
			
		||||
          this.rowClicked.emit(value)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onOpenChange(open: boolean) {
 | 
			
		||||
    this.open = open
 | 
			
		||||
    this.openChange.emit(open)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,14 @@
 | 
			
		||||
import { DSMeta } from 'src/app/models/sas/editors-getdata.model'
 | 
			
		||||
 | 
			
		||||
export interface DSMetaGroupped {
 | 
			
		||||
  group: string
 | 
			
		||||
  dsmeta: DSMeta[]
 | 
			
		||||
export interface Tab<T> {
 | 
			
		||||
  name: string
 | 
			
		||||
  title: string
 | 
			
		||||
  /**
 | 
			
		||||
   * Columns to be displayed in the the grid
 | 
			
		||||
   * If empty, all columns will be displayed
 | 
			
		||||
   */
 | 
			
		||||
  colsToDisplay: {
 | 
			
		||||
    colKey: string
 | 
			
		||||
    colName?: string
 | 
			
		||||
  }[]
 | 
			
		||||
  meta: T[]
 | 
			
		||||
  onRowClick?: (value: any) => void
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -106,7 +106,7 @@
 | 
			
		||||
            *clrIfOpen
 | 
			
		||||
          >
 | 
			
		||||
            <span *ngIf="tableLocked">
 | 
			
		||||
              To unlock all tables, contact support@datacontroller.io
 | 
			
		||||
              To unlock all tables, contact support@datacontroller.io
 | 
			
		||||
            </span>
 | 
			
		||||
          </clr-tooltip-content>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ import { parseColType } from './utils/parseColType'
 | 
			
		||||
import { dqValidate } from './validations/dq-validation'
 | 
			
		||||
import { specialMissingNumericValidator } from './validations/hot-custom-validators'
 | 
			
		||||
import { applyNumericFormats } from './utils/applyNumericFormats'
 | 
			
		||||
import { CustomAutocompleteEditor } from './editors/numericAutocomplete'
 | 
			
		||||
 | 
			
		||||
export class DcValidator {
 | 
			
		||||
  private rules: DcValidation[] = []
 | 
			
		||||
@@ -38,6 +39,8 @@ export class DcValidator {
 | 
			
		||||
    dqData: DQData[],
 | 
			
		||||
    hotInstance?: Handsontable
 | 
			
		||||
  ) {
 | 
			
		||||
    this.registerCustomEditors()
 | 
			
		||||
 | 
			
		||||
    this.sasparams = sasparams
 | 
			
		||||
    this.hotInstance = hotInstance
 | 
			
		||||
    this.rules = parseColType(sasparams.COLTYPE)
 | 
			
		||||
@@ -51,6 +54,13 @@ export class DcValidator {
 | 
			
		||||
    this.setupValidations()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  registerCustomEditors() {
 | 
			
		||||
    Handsontable.editors.registerEditor(
 | 
			
		||||
      'autocomplete.custom',
 | 
			
		||||
      CustomAutocompleteEditor
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRules(): DcValidation[] {
 | 
			
		||||
    return this.rules
 | 
			
		||||
  }
 | 
			
		||||
@@ -262,6 +272,7 @@ export class DcValidator {
 | 
			
		||||
        if (source.length > 0) {
 | 
			
		||||
          this.rules[i].source = source
 | 
			
		||||
          this.rules[i].type = 'autocomplete'
 | 
			
		||||
          this.rules[i].editor = 'autocomplete.custom'
 | 
			
		||||
          this.rules[i].filter = false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -315,7 +326,10 @@ export class DcValidator {
 | 
			
		||||
 | 
			
		||||
      // Because of dynamic cell validation, that will change the type of cell to dropdown
 | 
			
		||||
      // `rules[i].colType` could be different type (eg. numeric). So we check if current cell is dropdown, to call HOT native dropdown validator
 | 
			
		||||
      if (this.editor === 'autocomplete') {
 | 
			
		||||
      if (
 | 
			
		||||
        this.editor === 'autocomplete' ||
 | 
			
		||||
        this.editor === 'autocomplete.custom'
 | 
			
		||||
      ) {
 | 
			
		||||
        self
 | 
			
		||||
          .getHandsontableValidator('autocomplete')
 | 
			
		||||
          .call(this, value, (valid: boolean) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,28 @@
 | 
			
		||||
import Handsontable from 'handsontable'
 | 
			
		||||
import Core from 'handsontable/core'
 | 
			
		||||
 | 
			
		||||
export class CustomAutocompleteEditor extends Handsontable.editors
 | 
			
		||||
  .AutocompleteEditor {
 | 
			
		||||
  constructor(instance: Core) {
 | 
			
		||||
    super(instance)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createElements() {
 | 
			
		||||
    super.createElements()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Listbox open
 | 
			
		||||
  open(event?: Event | undefined): void {
 | 
			
		||||
    super.open(event)
 | 
			
		||||
 | 
			
		||||
    if (this.isCellNumeric()) {
 | 
			
		||||
      this.htContainer.classList.add('numericListbox')
 | 
			
		||||
    } else {
 | 
			
		||||
      this.htContainer.classList.remove('numericListbox')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isCellNumeric() {
 | 
			
		||||
    return this.cellProperties?.className?.includes('htNumeric')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,6 +11,8 @@ $clr-green: #60b515;
 | 
			
		||||
  height: $clr-header-height;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  margin-right: 10px;
 | 
			
		||||
 | 
			
		||||
  .spinner {
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,6 @@
 | 
			
		||||
        *ngFor="let programLog of sasjsRequests; let i = index"
 | 
			
		||||
        [id]="'request_' + i"
 | 
			
		||||
        [clrStackViewLevel]="1"
 | 
			
		||||
        [clrStackViewSetsize]="3"
 | 
			
		||||
        [clrStackViewPosinset]="3"
 | 
			
		||||
      >
 | 
			
		||||
        <clr-stack-label>
 | 
			
		||||
          {{ programLog.serviceLink }}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import { LoadingIndicatorComponent } from './loading-indicator/loading-indicator
 | 
			
		||||
import { LoginComponent } from './login/login.component'
 | 
			
		||||
import { UserService } from './user.service'
 | 
			
		||||
import { AlertsService } from './alerts/alerts.service'
 | 
			
		||||
import { UserNavDropdownComponent } from './user-nav-dropdown/user-nav-dropdown.component'
 | 
			
		||||
import { HeaderActions } from './user-nav-dropdown/header-actions.component'
 | 
			
		||||
import { AlertsComponent } from './alerts/alerts.component'
 | 
			
		||||
import { TermsComponent } from './terms/terms.component'
 | 
			
		||||
import { DirectivesModule } from '../directives/directives.module'
 | 
			
		||||
@@ -26,7 +26,7 @@ import { ContactLinkComponent } from './contact-link/contact-link.component'
 | 
			
		||||
  declarations: [
 | 
			
		||||
    LoadingIndicatorComponent,
 | 
			
		||||
    LoginComponent,
 | 
			
		||||
    UserNavDropdownComponent,
 | 
			
		||||
    HeaderActions,
 | 
			
		||||
    AlertsComponent,
 | 
			
		||||
    TermsComponent,
 | 
			
		||||
    DatasetInfoComponent,
 | 
			
		||||
@@ -35,7 +35,7 @@ import { ContactLinkComponent } from './contact-link/contact-link.component'
 | 
			
		||||
  exports: [
 | 
			
		||||
    LoadingIndicatorComponent,
 | 
			
		||||
    LoginComponent,
 | 
			
		||||
    UserNavDropdownComponent,
 | 
			
		||||
    HeaderActions,
 | 
			
		||||
    AlertsComponent,
 | 
			
		||||
    TermsComponent,
 | 
			
		||||
    DatasetInfoComponent,
 | 
			
		||||
 
 | 
			
		||||
@@ -107,7 +107,29 @@
 | 
			
		||||
      </clr-tab-content>
 | 
			
		||||
    </clr-tab>
 | 
			
		||||
  </clr-tabs>
 | 
			
		||||
  <p *ngIf="isMainRoute('home')" class="page-title">Edit</p>
 | 
			
		||||
 | 
			
		||||
  <div
 | 
			
		||||
    *ngIf="isMainRoute('home')"
 | 
			
		||||
    class="d-flex justify-content-center sub-dropdown"
 | 
			
		||||
  >
 | 
			
		||||
    <clr-dropdown>
 | 
			
		||||
      <button class="dropdown-toggle btn btn-link" clrDropdownTrigger>
 | 
			
		||||
        {{ getSubPage() }}
 | 
			
		||||
        <clr-icon shape="caret down"></clr-icon>
 | 
			
		||||
      </button>
 | 
			
		||||
      <clr-dropdown-menu *clrIfOpen>
 | 
			
		||||
        <a
 | 
			
		||||
          clrVerticalNavLink
 | 
			
		||||
          routerLink="/home/tables"
 | 
			
		||||
          routerLinkActive="active"
 | 
			
		||||
          >Tables</a
 | 
			
		||||
        >
 | 
			
		||||
        <a clrVerticalNavLink routerLink="/home/files" routerLinkActive="active"
 | 
			
		||||
          >Files</a
 | 
			
		||||
        >
 | 
			
		||||
      </clr-dropdown-menu>
 | 
			
		||||
    </clr-dropdown>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="nav-divider"></div>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,22 @@
 | 
			
		||||
<label *ngIf="label" class="clr-control-label">{{ label }}</label>
 | 
			
		||||
<label
 | 
			
		||||
  *ngIf="label"
 | 
			
		||||
  [class.secondLabelActive]="secondLabel && secondLabel.length > 0"
 | 
			
		||||
  class="clr-control-label"
 | 
			
		||||
>
 | 
			
		||||
  <span
 | 
			
		||||
    (click)="onChangeLabel('first')"
 | 
			
		||||
    [class.value-type-selected]="labelSelected === 'first'"
 | 
			
		||||
    >{{ label }}</span
 | 
			
		||||
  >
 | 
			
		||||
  <ng-container *ngIf="secondLabel">
 | 
			
		||||
    /
 | 
			
		||||
    <span
 | 
			
		||||
      (click)="onChangeLabel('second')"
 | 
			
		||||
      [class.value-type-selected]="labelSelected === 'second'"
 | 
			
		||||
      >{{ secondLabel }}</span
 | 
			
		||||
    >
 | 
			
		||||
  </ng-container>
 | 
			
		||||
</label>
 | 
			
		||||
<ng-container [ngSwitch]="type">
 | 
			
		||||
  <ng-container *ngSwitchCase="'date'">
 | 
			
		||||
    <clr-date-container>
 | 
			
		||||
 
 | 
			
		||||
@@ -28,4 +28,12 @@ clr-date-container {
 | 
			
		||||
            margin-top: -5px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
label.secondLabelActive span {
 | 
			
		||||
    &:not(.value-type-selected) {
 | 
			
		||||
        text-decoration: line-through;
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        opacity: 0.6;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -18,6 +18,7 @@ import { OnLoadingMoreEvent } from '../autocomplete/autocomplete.component'
 | 
			
		||||
export class SoftSelectComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() inputId: string = ''
 | 
			
		||||
  @Input() label: string | undefined
 | 
			
		||||
  @Input() secondLabel: string | undefined
 | 
			
		||||
  @Input() value: Date | string | null = ''
 | 
			
		||||
  @Input() disabled: boolean = false
 | 
			
		||||
  @Input() type: string = 'text'
 | 
			
		||||
@@ -30,20 +31,24 @@ export class SoftSelectComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Output() focusinInput: EventEmitter<any> = new EventEmitter()
 | 
			
		||||
  @Output() onAutocompleteLoadingMore: EventEmitter<OnLoadingMoreEvent> =
 | 
			
		||||
    new EventEmitter()
 | 
			
		||||
  @Output() selectedLabelChange: EventEmitter<string> = new EventEmitter()
 | 
			
		||||
 | 
			
		||||
  @ViewChild('input') inputElement: any
 | 
			
		||||
 | 
			
		||||
  temp: Date | string | null = ''
 | 
			
		||||
  inputFocused: boolean = false
 | 
			
		||||
 | 
			
		||||
  labelSelected: LabelTypes = 'first'
 | 
			
		||||
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(changes: SimpleChanges): void {
 | 
			
		||||
    if (
 | 
			
		||||
      changes.value &&
 | 
			
		||||
      changes.value.currentValue !== changes.value.previousValue
 | 
			
		||||
    )
 | 
			
		||||
    ) {
 | 
			
		||||
      this.valueChange.emit(changes.value.currentValue)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {}
 | 
			
		||||
@@ -85,4 +90,14 @@ export class SoftSelectComponent implements OnInit, OnChanges {
 | 
			
		||||
  onFocusinInput(event: any) {
 | 
			
		||||
    this.focusinInput.emit(event)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onChangeLabel(label: LabelTypes) {
 | 
			
		||||
    this.labelSelected = label
 | 
			
		||||
 | 
			
		||||
    const selectedLabelText = label === 'first' ? this.label : this.secondLabel
 | 
			
		||||
 | 
			
		||||
    this.selectedLabelChange.emit(selectedLabelText)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type LabelTypes = 'first' | 'second'
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
<div class="header-actions">
 | 
			
		||||
  <app-loading-indicator></app-loading-indicator>
 | 
			
		||||
 | 
			
		||||
  <clr-dropdown class="app-nav-dropdown">
 | 
			
		||||
    <button class="nav-text color-white" clrDropdownToggle>
 | 
			
		||||
      <span>{{ userName }}</span>
 | 
			
		||||
      <span *ngIf="userName !== 'Not logged in' && isViya"
 | 
			
		||||
        ><img class="avatar-img" src="{{ getPictureUrl() }}" alt=""
 | 
			
		||||
      /></span>
 | 
			
		||||
      <span
 | 
			
		||||
        class="badge badge-danger"
 | 
			
		||||
        *ngIf="!sasjsConfig.debug"
 | 
			
		||||
        [class.hidden]="failedReqs.length === 0"
 | 
			
		||||
        >{{ failedReqs.length }}</span
 | 
			
		||||
      >
 | 
			
		||||
      <span
 | 
			
		||||
        class="badge badge-info"
 | 
			
		||||
        *ngIf="sasjsConfig.debug"
 | 
			
		||||
        [class.hidden]="debugLogs.length === 0"
 | 
			
		||||
        >{{ debugLogs.length }}</span
 | 
			
		||||
      >
 | 
			
		||||
      <clr-icon *ngIf="!isViya" shape="caret down"></clr-icon>
 | 
			
		||||
    </button>
 | 
			
		||||
    <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
 | 
			
		||||
      <div #dropdownItemDebug class="debug-switch-item" clrDropdownItem>
 | 
			
		||||
        <clr-toggle-container
 | 
			
		||||
          class="toggle-switch"
 | 
			
		||||
          (click)="onDebugRowClick($event, dropdownItemDebug)"
 | 
			
		||||
        >
 | 
			
		||||
          <clr-toggle-wrapper>
 | 
			
		||||
            <input
 | 
			
		||||
              id="debug-toggle1"
 | 
			
		||||
              type="checkbox"
 | 
			
		||||
              [(ngModel)]="sasjsConfig.debug"
 | 
			
		||||
              (ngModelChange)="onDebugModeChange()"
 | 
			
		||||
              clrToggle
 | 
			
		||||
            />
 | 
			
		||||
            <label>Debug Mode</label>
 | 
			
		||||
          </clr-toggle-wrapper>
 | 
			
		||||
        </clr-toggle-container>
 | 
			
		||||
      </div>
 | 
			
		||||
      <a (click)="openRequestsModal()" clrDropdownItem>
 | 
			
		||||
        <span>SAS Requests</span>
 | 
			
		||||
      </a>
 | 
			
		||||
 | 
			
		||||
      <ng-container *ngIf="!isDeployPage">
 | 
			
		||||
        <a
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          href="https://docs.datacontroller.io"
 | 
			
		||||
          clrDropdownItem
 | 
			
		||||
        >
 | 
			
		||||
          <span class="dropdown-text">Documentation</span>
 | 
			
		||||
        </a>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
 | 
			
		||||
      <div class="separator"></div>
 | 
			
		||||
      <a href="..." routerLink="/system" clrDropdownItem>
 | 
			
		||||
        <span>System</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="..." (click)="logout($event)" clrDropdownItem>
 | 
			
		||||
        <span>Log Out</span>
 | 
			
		||||
        <clr-icon class="clr-logout" shape="logout"></clr-icon>
 | 
			
		||||
      </a>
 | 
			
		||||
      <div class="copyRight">
 | 
			
		||||
        <span>v{{ commitVer }}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </clr-dropdown-menu>
 | 
			
		||||
  </clr-dropdown>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,17 +1,12 @@
 | 
			
		||||
// it must be a better way to read clarity variables...
 | 
			
		||||
//@import '../../../../node_modules/@clr/ui/src/utils/helpers.clarity';
 | 
			
		||||
 | 
			
		||||
//@import '../../../../node_modules/@clr/ui/src/color/utils/colors.clarity';
 | 
			
		||||
//@import '../../../../node_modules/@clr/ui/src/color/utils/contrast-cache.clarity';
 | 
			
		||||
//@import '../../../../node_modules/@clr/ui/src/color/utils/helpers.clarity';
 | 
			
		||||
 | 
			
		||||
//@import '../../../../node_modules/@clr/ui/src/utils/variables.clarity';
 | 
			
		||||
 | 
			
		||||
$clr-header-height: 3rem;
 | 
			
		||||
$clr-near-white: #fafafa;
 | 
			
		||||
$clr-dark-gray: #565656;
 | 
			
		||||
$clr-light-gray: #eee;
 | 
			
		||||
 | 
			
		||||
:host {
 | 
			
		||||
  display: contents;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.copyRight {
 | 
			
		||||
  margin-top: 10px;
 | 
			
		||||
 | 
			
		||||
@@ -8,11 +8,11 @@ import { EventService } from '../../services/event.service'
 | 
			
		||||
import { Router } from '@angular/router'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-user-nav-dropdown',
 | 
			
		||||
  templateUrl: './user-nav-dropdown.component.html',
 | 
			
		||||
  styleUrls: ['./user-nav-dropdown.component.scss']
 | 
			
		||||
  selector: 'app-header-actions',
 | 
			
		||||
  templateUrl: './header-actions.component.html',
 | 
			
		||||
  styleUrls: ['./header-actions.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class UserNavDropdownComponent implements OnInit, OnDestroy {
 | 
			
		||||
export class HeaderActions implements OnInit, OnDestroy {
 | 
			
		||||
  public userName: string = 'Not logged in'
 | 
			
		||||
  private reqSub: Subscription = new Subscription()
 | 
			
		||||
  private userSub: Subscription = new Subscription()
 | 
			
		||||
@@ -1,121 +0,0 @@
 | 
			
		||||
<clr-dropdown class="app-nav-dropdown d-md-block">
 | 
			
		||||
  <button class="nav-text color-white" clrDropdownToggle>
 | 
			
		||||
    <span>{{ userName }}</span>
 | 
			
		||||
    <span *ngIf="userName !== 'Not logged in' && isViya"
 | 
			
		||||
      ><img class="avatar-img" src="{{ getPictureUrl() }}" alt=""
 | 
			
		||||
    /></span>
 | 
			
		||||
    <span
 | 
			
		||||
      class="badge badge-danger"
 | 
			
		||||
      *ngIf="!sasjsConfig.debug"
 | 
			
		||||
      [class.hidden]="failedReqs.length === 0"
 | 
			
		||||
      >{{ failedReqs.length }}</span
 | 
			
		||||
    >
 | 
			
		||||
    <span
 | 
			
		||||
      class="badge badge-info"
 | 
			
		||||
      *ngIf="sasjsConfig.debug"
 | 
			
		||||
      [class.hidden]="debugLogs.length === 0"
 | 
			
		||||
      >{{ debugLogs.length }}</span
 | 
			
		||||
    >
 | 
			
		||||
    <clr-icon *ngIf="!isViya" shape="caret down"></clr-icon>
 | 
			
		||||
  </button>
 | 
			
		||||
  <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
 | 
			
		||||
    <div #dropdownItemDebug class="debug-switch-item" clrDropdownItem>
 | 
			
		||||
      <clr-toggle-container
 | 
			
		||||
        class="toggle-switch"
 | 
			
		||||
        (click)="onDebugRowClick($event, dropdownItemDebug)"
 | 
			
		||||
      >
 | 
			
		||||
        <clr-toggle-wrapper>
 | 
			
		||||
          <input
 | 
			
		||||
            id="debug-toggle1"
 | 
			
		||||
            type="checkbox"
 | 
			
		||||
            [(ngModel)]="sasjsConfig.debug"
 | 
			
		||||
            (ngModelChange)="onDebugModeChange()"
 | 
			
		||||
            clrToggle
 | 
			
		||||
          />
 | 
			
		||||
          <label>Debug Mode</label>
 | 
			
		||||
        </clr-toggle-wrapper>
 | 
			
		||||
      </clr-toggle-container>
 | 
			
		||||
    </div>
 | 
			
		||||
    <a (click)="openRequestsModal()" clrDropdownItem>
 | 
			
		||||
      <span>SAS Requests</span>
 | 
			
		||||
    </a>
 | 
			
		||||
 | 
			
		||||
    <ng-container *ngIf="!isDeployPage">
 | 
			
		||||
      <a target="_blank" href="https://docs.datacontroller.io" clrDropdownItem>
 | 
			
		||||
        <span class="dropdown-text">Documentation</span>
 | 
			
		||||
      </a>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <div class="separator"></div>
 | 
			
		||||
    <a href="..." routerLink="/system" clrDropdownItem>
 | 
			
		||||
      <span>System</span>
 | 
			
		||||
    </a>
 | 
			
		||||
    <a href="..." (click)="logout($event)" clrDropdownItem>
 | 
			
		||||
      <span>Log Out</span>
 | 
			
		||||
      <clr-icon class="clr-logout" shape="logout"></clr-icon>
 | 
			
		||||
    </a>
 | 
			
		||||
    <div class="copyRight">
 | 
			
		||||
      <span>v{{ commitVer }}</span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </clr-dropdown-menu>
 | 
			
		||||
</clr-dropdown>
 | 
			
		||||
<div class="content-container h-auto">
 | 
			
		||||
  <nav class="sidenav d-block d-md-none" [clr-nav-level]="2">
 | 
			
		||||
    <section class="sidenav-content">
 | 
			
		||||
      <a href="..." class="nav-link active">
 | 
			
		||||
        {{ userName }}
 | 
			
		||||
      </a>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        <form>
 | 
			
		||||
          <div class="toggle-switch">
 | 
			
		||||
            <input
 | 
			
		||||
              id="debug-toggle2"
 | 
			
		||||
              type="checkbox"
 | 
			
		||||
              [(ngModel)]="sasjsConfig.debug"
 | 
			
		||||
              (ngModelChange)="onDebugModeChange()"
 | 
			
		||||
              [ngModelOptions]="{ standalone: true }"
 | 
			
		||||
            />
 | 
			
		||||
            <label
 | 
			
		||||
              for="debug-toggle2"
 | 
			
		||||
              class="debug-toggle-label color-dark-gray"
 | 
			
		||||
              >Debug Mode</label
 | 
			
		||||
            >
 | 
			
		||||
          </div>
 | 
			
		||||
        </form>
 | 
			
		||||
      </div>
 | 
			
		||||
      <!-- <a href="..." class="nav-link d-block" [routerLink]="['/application-logs']">
 | 
			
		||||
        <span>Application Logs</span>
 | 
			
		||||
        <span class="badge" *ngIf="appLogs.length > 0">{{appLogs.length}}</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a *ngIf="debugMode" class="nav-link d-block" href="..." [routerLink]="['/debug-logs']">
 | 
			
		||||
        <span>Debug Logs</span>
 | 
			
		||||
        <span class="badge badge-info" *ngIf="debugLogs.length > 0">{{debugLogs.length}}</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a *ngIf="!debugMode" class="nav-link d-block" href="..." [routerLink]="['/failed-requests']">
 | 
			
		||||
        <span>Failed Requests</span>
 | 
			
		||||
        <span class="badge badge-danger" *ngIf="failedReqs.length > 0">{{failedReqs.length}}</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="..." class="nav-link d-block" [routerLink]="['/errors']">
 | 
			
		||||
        <span>Errors</span>
 | 
			
		||||
        <span class="badge badge-warning" *ngIf="sasErrors.length > 0">{{sasErrors.length}}</span>
 | 
			
		||||
      </a> -->
 | 
			
		||||
      <a
 | 
			
		||||
        class="nav-link d-block"
 | 
			
		||||
        target="_blank"
 | 
			
		||||
        href="https://docs.datacontroller.io"
 | 
			
		||||
      >
 | 
			
		||||
        <span>Documentation</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <div class="separator"></div>
 | 
			
		||||
      <a routerLink="/system" class="nav-link d-block">
 | 
			
		||||
        <span>System</span>
 | 
			
		||||
        <clr-icon shape="logout"></clr-icon>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="..." class="nav-link d-block" (click)="logout($event)">
 | 
			
		||||
        <span>Log Out</span>
 | 
			
		||||
        <clr-icon shape="logout"></clr-icon>
 | 
			
		||||
      </a>
 | 
			
		||||
    </section>
 | 
			
		||||
  </nav>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -13,7 +13,7 @@
 | 
			
		||||
      class="licence-notice"
 | 
			
		||||
      >To unlock more then {{ licenceState.value.viewbox_limit }}
 | 
			
		||||
      {{ licenceState.value.viewbox_limit === 1 ? 'viewbox' : 'viewboxes' }},
 | 
			
		||||
      contact support@datacontroller.io</span
 | 
			
		||||
      contact support@datacontroller.io</span
 | 
			
		||||
    >
 | 
			
		||||
  </h3>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -58,34 +58,57 @@
 | 
			
		||||
          <div class="mt-20">
 | 
			
		||||
            <div class="row">
 | 
			
		||||
              <button
 | 
			
		||||
                class="btn btn-sm btn-outline text-center mt-20"
 | 
			
		||||
                class="btn btn-sm btn-outline text-center mr-5i"
 | 
			
		||||
                (click)="viewerTableScreen()"
 | 
			
		||||
                [disabled]="revertingChanges"
 | 
			
		||||
              >
 | 
			
		||||
                Go to base table screen
 | 
			
		||||
                View base table
 | 
			
		||||
              </button>
 | 
			
		||||
              <button
 | 
			
		||||
                id="approval-btn"
 | 
			
		||||
                class="btn btn-sm btn-success-outline text-center mt-20"
 | 
			
		||||
                class="btn btn-sm btn-success-outline text-center mr-5i"
 | 
			
		||||
                [disabled]="
 | 
			
		||||
                  tableDetails?.REVIEW_STATUS_ID === 'APPROVED' ||
 | 
			
		||||
                  tableDetails?.REVIEW_STATUS_ID === 'REJECTED'
 | 
			
		||||
                "
 | 
			
		||||
                (click)="approveTableScreen()"
 | 
			
		||||
                [disabled]="revertingChanges"
 | 
			
		||||
              >
 | 
			
		||||
                Go to approvals screen
 | 
			
		||||
                Approve
 | 
			
		||||
              </button>
 | 
			
		||||
              <button
 | 
			
		||||
                class="btn btn-sm btn-info-outline text-center mt-20"
 | 
			
		||||
                class="btn btn-sm btn-info-outline text-center mr-5i"
 | 
			
		||||
                (click)="goBack()"
 | 
			
		||||
                [disabled]="revertingChanges"
 | 
			
		||||
              >
 | 
			
		||||
                Go back to editor
 | 
			
		||||
                Edit base table
 | 
			
		||||
              </button>
 | 
			
		||||
              <button
 | 
			
		||||
                class="btn btn-sm btn-success text-center mt-20 min-w-0"
 | 
			
		||||
                class="btn btn-sm btn-success text-center mr-5i min-w-0"
 | 
			
		||||
                (click)="download(tableDetails?.TABLE_ID)"
 | 
			
		||||
              >
 | 
			
		||||
                <clr-icon shape="download"></clr-icon>
 | 
			
		||||
              </button>
 | 
			
		||||
 | 
			
		||||
              <clr-tooltip>
 | 
			
		||||
                <button
 | 
			
		||||
                  *ngIf="tableDetails?.['ALLOW_RESTORE'] === 'YES'"
 | 
			
		||||
                  (click)="revertChanges()"
 | 
			
		||||
                  clrTooltipTrigger
 | 
			
		||||
                  [clrLoading]="revertingChanges"
 | 
			
		||||
                  class="btn btn-sm btn-danger text-center mt-20"
 | 
			
		||||
                >
 | 
			
		||||
                  REVERT
 | 
			
		||||
 | 
			
		||||
                  <clr-tooltip-content
 | 
			
		||||
                    clrPosition="bottom-left"
 | 
			
		||||
                    clrSize="lg"
 | 
			
		||||
                    *clrIfOpen
 | 
			
		||||
                  >
 | 
			
		||||
                    <span> Revert this and all subsequent changes </span>
 | 
			
		||||
                  </clr-tooltip-content>
 | 
			
		||||
                </button>
 | 
			
		||||
              </clr-tooltip>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,10 @@ import { Router } from '@angular/router'
 | 
			
		||||
import { ActivatedRoute } from '@angular/router'
 | 
			
		||||
import { SasService } from '../services/sas.service'
 | 
			
		||||
import { EventService } from '../services/event.service'
 | 
			
		||||
import { AppService } from '../services/app.service'
 | 
			
		||||
import { HotTableInterface } from '../models/HotTable.interface'
 | 
			
		||||
import { LicenceService } from '../services/licence.service'
 | 
			
		||||
import { globals } from '../_globals'
 | 
			
		||||
import { EditorsRestoreServiceResponse } from '../models/sas/editors-restore.model'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-stage',
 | 
			
		||||
@@ -22,6 +23,7 @@ export class StageComponent implements OnInit {
 | 
			
		||||
  public keysArray: any
 | 
			
		||||
  public tableDetails: any
 | 
			
		||||
  public loaded: boolean = false
 | 
			
		||||
  public revertingChanges: boolean = false
 | 
			
		||||
  public licenceState = this.licenceService.licenceState
 | 
			
		||||
  public hotTable: HotTableInterface = {
 | 
			
		||||
    data: [],
 | 
			
		||||
@@ -55,7 +57,15 @@ export class StageComponent implements OnInit {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public goBack() {
 | 
			
		||||
    this.route.navigateByUrl('/editor/' + this.tableDetails.BASE_TABLE)
 | 
			
		||||
    const xlmap = globals.xlmaps.find(
 | 
			
		||||
      (xlmap) => xlmap.targetDS === this.tableDetails.BASE_TABLE
 | 
			
		||||
    )
 | 
			
		||||
    if (xlmap) {
 | 
			
		||||
      const id = this.hotTable.data[0].XLMAP_ID
 | 
			
		||||
      this.route.navigateByUrl('/home/files/' + id)
 | 
			
		||||
    } else {
 | 
			
		||||
      this.route.navigateByUrl('/editor/' + this.tableDetails.BASE_TABLE)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public download(id: any) {
 | 
			
		||||
@@ -153,6 +163,31 @@ export class StageComponent implements OnInit {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  revertChanges() {
 | 
			
		||||
    this.revertingChanges = true
 | 
			
		||||
 | 
			
		||||
    const data = {
 | 
			
		||||
      restore_in: [
 | 
			
		||||
        {
 | 
			
		||||
          load_ref: this.table_id
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.sasService
 | 
			
		||||
      .request('editors/restore', data)
 | 
			
		||||
      .then((res: EditorsRestoreServiceResponse) => {
 | 
			
		||||
        if (res.restore_out) {
 | 
			
		||||
          this.route.navigate([`/stage`]).then(() => {
 | 
			
		||||
            this.route.navigate([`/stage/${res.restore_out[0].LOADREF}`])
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => {
 | 
			
		||||
        this.revertingChanges = false
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setFocus() {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      let approvalBtn: any = window.document.getElementById('approval-btn')
 | 
			
		||||
 
 | 
			
		||||
@@ -9,43 +9,45 @@
 | 
			
		||||
        class="sys-info d-flex clr-justify-content-center clr-flex-column clr-flex-lg-row"
 | 
			
		||||
      >
 | 
			
		||||
        <div>
 | 
			
		||||
          <h6 class="m-0">Environment Details <span class="dark"></span></h6>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <h6 cds-text="subsection" class="mb-10">
 | 
			
		||||
            Environment Details <span class="dark"></span>
 | 
			
		||||
          </h6>
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            SYSSITE: <span class="dark">{{ environmentInfo?.SYSSITE }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            SYSSCPL: <span class="dark">{{ environmentInfo?.SYSSCPL }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            SYSTCPIPHOSTNAME:
 | 
			
		||||
            <span class="dark">{{ environmentInfo?.SYSTCPIPHOSTNAME }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            SYSVLONG: <span class="dark">{{ environmentInfo?.SYSVLONG }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            MEMSIZE: <span class="dark">{{ environmentInfo?.MEMSIZE }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            SYSPROCESSMODE:
 | 
			
		||||
            <span class="dark">{{ environmentInfo?.SYSPROCESSMODE }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            SYSHOSTNAME:
 | 
			
		||||
            <span class="dark">{{ environmentInfo?.SYSHOSTNAME }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            SYSHOSTINFOLONG:
 | 
			
		||||
            <span class="dark">{{ environmentInfo?.SYSHOSTINFOLONG }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            SYSENCODING:
 | 
			
		||||
            <span class="dark">{{ environmentInfo?.SYSENCODING }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            AUTOEXEC: <span class="dark">{{ environmentInfo?.AUTOEXEC }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            DC ADMIN GROUP:
 | 
			
		||||
            <span class="dark">{{ environmentInfo?.DC_ADMIN_GROUP }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
@@ -53,42 +55,44 @@
 | 
			
		||||
 | 
			
		||||
        <div class="d-flex clr-justify-content-lg-center">
 | 
			
		||||
          <div>
 | 
			
		||||
            <h6 class="m-0">
 | 
			
		||||
            <h6 cds-text="subsection" class="mb-10">
 | 
			
		||||
              Data Controller Details <span class="dark"></span>
 | 
			
		||||
            </h6>
 | 
			
		||||
            <p class="m-0">
 | 
			
		||||
            <p cds-text="label" class="m-0">
 | 
			
		||||
              Application version:
 | 
			
		||||
              <span class="dark">{{ appInfo.appVersion }}</span>
 | 
			
		||||
            </p>
 | 
			
		||||
            <p class="m-0">
 | 
			
		||||
            <p cds-text="label" class="m-0">
 | 
			
		||||
              Build timestamp:
 | 
			
		||||
              <span class="dark">{{ appInfo.buildTimestamp }}</span>
 | 
			
		||||
            </p>
 | 
			
		||||
            <p class="m-0">
 | 
			
		||||
            <p cds-text="label" class="m-0">
 | 
			
		||||
              Adapter version:
 | 
			
		||||
              <span class="dark">{{ appInfo.adapterVersion }}</span>
 | 
			
		||||
            </p>
 | 
			
		||||
            <p class="m-0">
 | 
			
		||||
            <p cds-text="label" class="m-0">
 | 
			
		||||
              HTTP: <span class="dark">{{ http ? 'YES' : 'NO' }}</span>
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
          <h6 class="m-0">Licence details <span class="dark"></span></h6>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <h6 cds-text="subsection" class="mb-10">
 | 
			
		||||
            Licence details <span class="dark"></span>
 | 
			
		||||
          </h6>
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            Valid until:
 | 
			
		||||
            <span class="dark">{{ licenceInfo?.valid_until }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            Users allowed:
 | 
			
		||||
            <span class="dark">{{ licenceInfo?.users_allowed }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            Site IDs:
 | 
			
		||||
            <span class="dark">{{ licenceInfo?.site_id_multiple }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            Free Tier:
 | 
			
		||||
            <span class="dark">{{ licenceInfo?.demo ? 'YES' : 'NO' }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
@@ -157,25 +161,25 @@
 | 
			
		||||
              licenceState.value.lineage_daily_limit
 | 
			
		||||
            }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            Viewboxes:
 | 
			
		||||
            <span class="dark">{{
 | 
			
		||||
              licenceState.value.viewbox ? 'YES' : 'NO'
 | 
			
		||||
            }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            File Upload:
 | 
			
		||||
            <span class="dark">{{
 | 
			
		||||
              licenceState.value.fileUpload ? 'YES' : 'NO'
 | 
			
		||||
            }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            Edit record:
 | 
			
		||||
            <span class="dark">{{
 | 
			
		||||
              licenceState.value.editRecord ? 'YES' : 'NO'
 | 
			
		||||
            }}</span>
 | 
			
		||||
          </p>
 | 
			
		||||
          <p class="m-0">
 | 
			
		||||
          <p cds-text="label" class="m-0">
 | 
			
		||||
            Add record:
 | 
			
		||||
            <span class="dark">{{
 | 
			
		||||
              licenceState.value.addRecord ? 'YES' : 'NO'
 | 
			
		||||
 
 | 
			
		||||
@@ -99,15 +99,18 @@
 | 
			
		||||
              </ng-container>
 | 
			
		||||
              {{ libTable.replace('-FC', '') }}
 | 
			
		||||
            </button>
 | 
			
		||||
            <clr-tooltip-content
 | 
			
		||||
              clrPosition="bottom-right"
 | 
			
		||||
              clrSize="lg"
 | 
			
		||||
              *clrIfOpen
 | 
			
		||||
            >
 | 
			
		||||
              <span *ngIf="tableLocked">
 | 
			
		||||
                To unlock all tables, contact support@datacontroller.io
 | 
			
		||||
              </span>
 | 
			
		||||
            </clr-tooltip-content>
 | 
			
		||||
 | 
			
		||||
            <ng-container *ngIf="tableLocked">
 | 
			
		||||
              <clr-tooltip-content
 | 
			
		||||
                clrPosition="bottom-right"
 | 
			
		||||
                clrSize="lg"
 | 
			
		||||
                *clrIfOpen
 | 
			
		||||
              >
 | 
			
		||||
                <span>
 | 
			
		||||
                  To unlock all tables, contact support@datacontroller.io
 | 
			
		||||
                </span>
 | 
			
		||||
              </clr-tooltip-content>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </clr-tooltip>
 | 
			
		||||
        </clr-tree-node>
 | 
			
		||||
      </clr-tree-node>
 | 
			
		||||
@@ -358,36 +361,52 @@
 | 
			
		||||
      </section>
 | 
			
		||||
 | 
			
		||||
      <div class="title-col clr-col-auto clr-flex-column clr-flex-sm-row">
 | 
			
		||||
        <clr-icon
 | 
			
		||||
          (click)="datasetInfo = true"
 | 
			
		||||
          shape="info-circle"
 | 
			
		||||
          class="is-highlight cursor-pointer"
 | 
			
		||||
          size="24"
 | 
			
		||||
        ></clr-icon>
 | 
			
		||||
 | 
			
		||||
        <clr-icon
 | 
			
		||||
          *ngIf="tableTitle?.includes('-FC')"
 | 
			
		||||
          shape="bolt"
 | 
			
		||||
          class="color-yellow mt-5 mr-5"
 | 
			
		||||
        ></clr-icon>
 | 
			
		||||
 | 
			
		||||
        <h3
 | 
			
		||||
          *ngIf="tableTitle && tableTitle.length > 0"
 | 
			
		||||
          class="viewerTitle clr-flex-column d-flex clr-flex-sm-row clr-align-items-center"
 | 
			
		||||
          class="viewerTitle clr-flex-column d-flex clr-flex-sm-row clr-align-items-center clr-justify-content-center"
 | 
			
		||||
        >
 | 
			
		||||
          {{ tableTitle?.replace('-FC', '') }}
 | 
			
		||||
          <clr-tooltip class="d-flex clr-align-items-center">
 | 
			
		||||
            <clr-icon
 | 
			
		||||
              clrTooltipTrigger
 | 
			
		||||
              (click)="datasetInfo = true"
 | 
			
		||||
              shape="info-circle"
 | 
			
		||||
              class="is-highlight cursor-pointer"
 | 
			
		||||
              size="24"
 | 
			
		||||
            ></clr-icon>
 | 
			
		||||
 | 
			
		||||
          <span *ngIf="numberOfRows !== null">
 | 
			
		||||
            ({{ numberOfRows | thousandSeparator: ',' }}
 | 
			
		||||
            {{ numberOfRows! === 1 ? 'row' : 'rows' }}, {{ filterCols.length
 | 
			
		||||
            }}{{ filterCols.length === 1 ? ' col' : ' cols' }})
 | 
			
		||||
          </span>
 | 
			
		||||
            <clr-icon
 | 
			
		||||
              *ngIf="tableTitle?.includes('-FC')"
 | 
			
		||||
              shape="bolt"
 | 
			
		||||
              class="color-yellow mr-5"
 | 
			
		||||
            ></clr-icon>
 | 
			
		||||
 | 
			
		||||
          <clr-icon
 | 
			
		||||
            (click)="reloadTableData()"
 | 
			
		||||
            class="refresh-table"
 | 
			
		||||
            shape="refresh"
 | 
			
		||||
          ></clr-icon>
 | 
			
		||||
            <span clrTooltipTrigger *ngIf="tableTitle && tableTitle.length > 0">
 | 
			
		||||
              {{ tableTitle?.replace('-FC', '') }}
 | 
			
		||||
            </span>
 | 
			
		||||
 | 
			
		||||
            <ng-container *ngIf="this.dsNote && this.dsNote.length > 0">
 | 
			
		||||
              <clr-tooltip-content
 | 
			
		||||
                clrPosition="bottom-left"
 | 
			
		||||
                clrSize="lg"
 | 
			
		||||
                *clrIfOpen
 | 
			
		||||
              >
 | 
			
		||||
                {{ this.dsNote }}
 | 
			
		||||
              </clr-tooltip-content>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </clr-tooltip>
 | 
			
		||||
 | 
			
		||||
          <ng-container *ngIf="tableTitle && tableTitle.length > 0">
 | 
			
		||||
            <span *ngIf="numberOfRows !== null">
 | 
			
		||||
              ({{ numberOfRows | thousandSeparator: ',' }}
 | 
			
		||||
              {{ numberOfRows! === 1 ? 'row' : 'rows' }}, {{ filterCols.length
 | 
			
		||||
              }}{{ filterCols.length === 1 ? ' col' : ' cols' }})
 | 
			
		||||
            </span>
 | 
			
		||||
 | 
			
		||||
            <clr-icon
 | 
			
		||||
              (click)="reloadTableData()"
 | 
			
		||||
              class="refresh-table"
 | 
			
		||||
              shape="refresh"
 | 
			
		||||
            ></clr-icon>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
        </h3>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@@ -406,62 +425,34 @@
 | 
			
		||||
            options
 | 
			
		||||
          </button>
 | 
			
		||||
          <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              class="btn btn-sm btn-success-outline"
 | 
			
		||||
              (click)="newViewbox()"
 | 
			
		||||
              clrDropdownItem
 | 
			
		||||
            >
 | 
			
		||||
            <div (click)="newViewbox()" clrDropdownItem>
 | 
			
		||||
              <clr-icon shape="view-cards"></clr-icon>
 | 
			
		||||
              <span>Viewboxes</span>
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              class="btn btn-sm btn-success-outline"
 | 
			
		||||
            </div>
 | 
			
		||||
            <div
 | 
			
		||||
              *ngIf="tableEditExists()"
 | 
			
		||||
              (click)="editTable()"
 | 
			
		||||
              clrDropdownItem
 | 
			
		||||
            >
 | 
			
		||||
              <clr-icon shape="pencil"></clr-icon>
 | 
			
		||||
              <span>Edit</span>
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              class="btn btn-sm btn-success-outline"
 | 
			
		||||
              *ngIf="tableuri"
 | 
			
		||||
              (click)="goToLineage()"
 | 
			
		||||
              clrDropdownItem
 | 
			
		||||
            >
 | 
			
		||||
            </div>
 | 
			
		||||
            <div *ngIf="tableuri" (click)="goToLineage()" clrDropdownItem>
 | 
			
		||||
              <clr-icon shape="switch"></clr-icon>
 | 
			
		||||
              <span>Lineage</span>
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              class="btn btn-sm btn-outline btn-block"
 | 
			
		||||
              (click)="openQb()"
 | 
			
		||||
              clrDropdownItem
 | 
			
		||||
            >
 | 
			
		||||
            </div>
 | 
			
		||||
            <div (click)="openQb()" clrDropdownItem>
 | 
			
		||||
              <clr-icon shape="filter"></clr-icon>
 | 
			
		||||
              <span>Filter</span>
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              class="btn btn-sm btn-success-outline"
 | 
			
		||||
              (click)="openDownload = true"
 | 
			
		||||
              clrDropdownItem
 | 
			
		||||
            >
 | 
			
		||||
            </div>
 | 
			
		||||
            <div (click)="openDownload = true" clrDropdownItem>
 | 
			
		||||
              <clr-icon shape="download"></clr-icon>
 | 
			
		||||
              <span>Download</span>
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              class="btn btn-sm btn-success-outline"
 | 
			
		||||
              (click)="showWebQuery()"
 | 
			
		||||
              clrDropdownItem
 | 
			
		||||
            >
 | 
			
		||||
            </div>
 | 
			
		||||
            <div (click)="showWebQuery()" clrDropdownItem>
 | 
			
		||||
              <clr-icon shape="download-cloud"></clr-icon>
 | 
			
		||||
              <span>Web Query URL</span>
 | 
			
		||||
            </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </clr-dropdown-menu>
 | 
			
		||||
        </clr-dropdown>
 | 
			
		||||
      </div>
 | 
			
		||||
@@ -630,6 +621,9 @@
 | 
			
		||||
        [cells]="hotTable.cells"
 | 
			
		||||
        [maxRows]="hotTable.maxRows"
 | 
			
		||||
        [manualColumnResize]="true"
 | 
			
		||||
        [rowHeaders]="hotTable.rowHeaders"
 | 
			
		||||
        [rowHeaderWidth]="hotTable.rowHeaderWidth"
 | 
			
		||||
        [rowHeights]="hotTable.rowHeights"
 | 
			
		||||
        [licenseKey]="hotTable.licenseKey"
 | 
			
		||||
      >
 | 
			
		||||
      </hot-table>
 | 
			
		||||
@@ -651,6 +645,12 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<app-dataset-info [(open)]="datasetInfo" [dsmeta]="dsmeta"></app-dataset-info>
 | 
			
		||||
<app-dataset-info
 | 
			
		||||
  [(open)]="datasetInfo"
 | 
			
		||||
  [dsmeta]="dsmeta"
 | 
			
		||||
  [versions]="versions"
 | 
			
		||||
  (rowClicked)="datasetInfoModalRowClicked($event)"
 | 
			
		||||
>
 | 
			
		||||
</app-dataset-info>
 | 
			
		||||
 | 
			
		||||
<app-viewboxes [(viewboxModal)]="viewboxOpen"></app-viewboxes>
 | 
			
		||||
 
 | 
			
		||||
@@ -57,6 +57,7 @@ clr-tree-node button {
 | 
			
		||||
.viewerTitle {
 | 
			
		||||
  text-align:center;
 | 
			
		||||
  margin-bottom: 15px;
 | 
			
		||||
  margin-top: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-menu {
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,11 @@ import { FilterGroup, FilterQuery } from '../models/FilterQuery'
 | 
			
		||||
import { HotTableInterface } from '../models/HotTable.interface'
 | 
			
		||||
import { LoggerService } from '../services/logger.service'
 | 
			
		||||
import Handsontable from 'handsontable'
 | 
			
		||||
import { $DataFormats, DSMeta } from '../models/sas/editors-getdata.model'
 | 
			
		||||
import {
 | 
			
		||||
  $DataFormats,
 | 
			
		||||
  DSMeta,
 | 
			
		||||
  Version
 | 
			
		||||
} from '../models/sas/editors-getdata.model'
 | 
			
		||||
import { mergeColsRules } from '../shared/dc-validator/utils/mergeColsRules'
 | 
			
		||||
import { PublicViewtablesServiceResponse } from '../models/sas/public-viewtables.model'
 | 
			
		||||
import { PublicViewlibsServiceResponse } from '../models/sas/public-viewlibs.model'
 | 
			
		||||
@@ -95,6 +99,8 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
 | 
			
		||||
  public $dataFormats: $DataFormats | null = null
 | 
			
		||||
  public datasetInfo: boolean = false
 | 
			
		||||
  public dsmeta: DSMeta[] = []
 | 
			
		||||
  public versions: Version[] = []
 | 
			
		||||
  public dsNote = ''
 | 
			
		||||
 | 
			
		||||
  public licenceState = this.licenceService.licenceState
 | 
			
		||||
  public Infinity = Infinity
 | 
			
		||||
@@ -108,6 +114,11 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
 | 
			
		||||
    settings: {},
 | 
			
		||||
    afterGetColHeader: undefined,
 | 
			
		||||
    licenseKey: undefined,
 | 
			
		||||
    rowHeaders: (index: number) => {
 | 
			
		||||
      return ' '
 | 
			
		||||
    },
 | 
			
		||||
    rowHeaderWidth: 15,
 | 
			
		||||
    rowHeights: 20,
 | 
			
		||||
    contextMenu: ['copy_with_column_headers', 'copy_column_headers_only'],
 | 
			
		||||
    copyPaste: {
 | 
			
		||||
      copyColumnHeaders: true,
 | 
			
		||||
@@ -241,6 +252,8 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
 | 
			
		||||
        this.hotTable.data = res.viewdata
 | 
			
		||||
        this.$dataFormats = res.$viewdata
 | 
			
		||||
        this.dsmeta = res.dsmeta
 | 
			
		||||
        this.versions = res.versions || []
 | 
			
		||||
        this.setDSNote()
 | 
			
		||||
        this.numberOfRows = res.sasparams[0].NOBS
 | 
			
		||||
        this.queryText = res.sasparams[0].FILTER_TEXT
 | 
			
		||||
        this.headerPks = res.sasparams[0].PK_FIELDS.split(' ')
 | 
			
		||||
@@ -798,6 +811,8 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
 | 
			
		||||
          this.hotTable.data = res.viewdata
 | 
			
		||||
          this.$dataFormats = res.$viewdata
 | 
			
		||||
          this.dsmeta = res.dsmeta
 | 
			
		||||
          this.versions = res.versions || []
 | 
			
		||||
          this.setDSNote()
 | 
			
		||||
          this.queryText = res.sasparams[0].FILTER_TEXT
 | 
			
		||||
          let columns: any[] = []
 | 
			
		||||
          let colArr = []
 | 
			
		||||
@@ -1011,6 +1026,32 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
 | 
			
		||||
    this.sasStoreService.removeClause()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public datasetInfoModalRowClicked(value: Version | DSMeta) {
 | 
			
		||||
    if ((<Version>value).LOAD_REF !== undefined) {
 | 
			
		||||
      // Type is Version
 | 
			
		||||
      const row = value as Version
 | 
			
		||||
      const url = `/stage/${row.LOAD_REF}`
 | 
			
		||||
 | 
			
		||||
      this.router.navigate([url])
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setDSNote() {
 | 
			
		||||
    const notes = this.dsmeta.find((item) => item.NAME === 'NOTES')
 | 
			
		||||
    const longDesc = this.dsmeta.find((item) => item.NAME === 'DD_LONGDESC')
 | 
			
		||||
    const shortDesc = this.dsmeta.find((item) => item.NAME === 'DD_SHORTDESC')
 | 
			
		||||
 | 
			
		||||
    if (notes && notes.VALUE) {
 | 
			
		||||
      this.dsNote = notes.VALUE
 | 
			
		||||
    } else if (longDesc && longDesc.VALUE) {
 | 
			
		||||
      this.dsNote = longDesc.VALUE
 | 
			
		||||
    } else if (shortDesc && shortDesc.VALUE) {
 | 
			
		||||
      this.dsNote = shortDesc.VALUE
 | 
			
		||||
    } else {
 | 
			
		||||
      this.dsNote = ''
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setupHot() {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      if (!this.loadingTableView && this.libDataset) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										159
									
								
								client/src/app/xlmap/tests/xl.utils.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								client/src/app/xlmap/tests/xl.utils.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,159 @@
 | 
			
		||||
import {
 | 
			
		||||
  extractRowAndCol,
 | 
			
		||||
  getCellAddress,
 | 
			
		||||
  getFinishingCell,
 | 
			
		||||
  isBlankRow
 | 
			
		||||
} from '../utils/xl.utils'
 | 
			
		||||
 | 
			
		||||
describe('isBlankRow', () => {
 | 
			
		||||
  it('should return true for a blank row', () => {
 | 
			
		||||
    const blankRow = { __rowNum__: 1 }
 | 
			
		||||
    expect(isBlankRow(blankRow)).toBeTrue()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should return false for a non-blank row', () => {
 | 
			
		||||
    const nonBlankRow = {
 | 
			
		||||
      B: 3,
 | 
			
		||||
      C: 'some value',
 | 
			
		||||
      D: -203
 | 
			
		||||
    }
 | 
			
		||||
    expect(isBlankRow(nonBlankRow)).toBeFalse()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
describe('extractRowAndCol', () => {
 | 
			
		||||
  it('should extract row and column from "MATCH F R[2]C[0]: CASH BALANCE"', () => {
 | 
			
		||||
    const input = 'MATCH F R[2]C[0]: CASH BALANCE'
 | 
			
		||||
    const result = extractRowAndCol(input)
 | 
			
		||||
    expect(result).toEqual({ row: 2, column: 0 })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should extract row and column from "RELATIVE R[10]C[6]"', () => {
 | 
			
		||||
    const input = 'RELATIVE R[10]C[6]'
 | 
			
		||||
    const result = extractRowAndCol(input)
 | 
			
		||||
    expect(result).toEqual({ row: 10, column: 6 })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should return null for invalid input', () => {
 | 
			
		||||
    const invalidInput = 'INVALID INPUT'
 | 
			
		||||
    const result = extractRowAndCol(invalidInput)
 | 
			
		||||
    expect(result).toBeNull()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
describe('getCellAddress', () => {
 | 
			
		||||
  const arrayOfObjects = [
 | 
			
		||||
    { A: 'valueA1', B: 'valueB1' },
 | 
			
		||||
    { A: 'valueA2', B: 'valueB2' }
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  it('should convert "ABSOLUTE D8" to A1-style address', () => {
 | 
			
		||||
    const input = 'ABSOLUTE D8'
 | 
			
		||||
    const result = getCellAddress(input, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('D8')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "RELATIVE R[10]C[6]" to A1-style address', () => {
 | 
			
		||||
    const input = 'RELATIVE R[10]C[6]'
 | 
			
		||||
    const result = getCellAddress(input, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('F10')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "MATCH 1 R[0]C[0]:valueA1" to A1-style address', () => {
 | 
			
		||||
    const input = 'MATCH 1 R[0]C[0]:valueA1'
 | 
			
		||||
    const result = getCellAddress(input, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('A1')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "MATCH A R[0]C[0]:valueA1" to A1-style address', () => {
 | 
			
		||||
    const input = 'MATCH A R[0]C[0]:valueA1'
 | 
			
		||||
    const result = getCellAddress(input, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('A1')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "MATCH 1 R[1]C[0]:valueA1" to A1-style address', () => {
 | 
			
		||||
    const input = 'MATCH 1 R[1]C[0]:valueA1'
 | 
			
		||||
    const result = getCellAddress(input, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('A2')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "MATCH A R[0]C[1]:valueA1" to A1-style address', () => {
 | 
			
		||||
    const input = 'MATCH A R[0]C[1]:valueA1'
 | 
			
		||||
    const result = getCellAddress(input, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('B1')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "MATCH 1 R[1]C[1]:valueA1" to A1-style address', () => {
 | 
			
		||||
    const input = 'MATCH 1 R[1]C[1]:valueA1'
 | 
			
		||||
    const result = getCellAddress(input, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('B2')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "MATCH A R[1]C[1]:valueA1" to A1-style address', () => {
 | 
			
		||||
    const input = 'MATCH A R[1]C[1]:valueA1'
 | 
			
		||||
    const result = getCellAddress(input, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('B2')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
describe('getFinishingCell', () => {
 | 
			
		||||
  const arrayOfObjects = [
 | 
			
		||||
    { A: 'valueA1', B: 'valueB1' },
 | 
			
		||||
    { A: 'valueA2', B: 'valueB2' },
 | 
			
		||||
    { A: 'valueA3', B: 'valueB3' },
 | 
			
		||||
    { B: 'valueB4' },
 | 
			
		||||
    { A: 'valueA5' },
 | 
			
		||||
    { A: 'valueA6', B: 'valueB6' },
 | 
			
		||||
    {},
 | 
			
		||||
    { A: 'valueA8' }
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  it('should return the start cell if finish is an empty string', () => {
 | 
			
		||||
    const start = 'A1'
 | 
			
		||||
    const finish = ''
 | 
			
		||||
    const result = getFinishingCell(start, finish, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe(start)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "ABSOLUTE D8" to A1-style address', () => {
 | 
			
		||||
    const start = 'A1'
 | 
			
		||||
    const finish = 'ABSOLUTE D8'
 | 
			
		||||
    const result = getFinishingCell(start, finish, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('D8')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "RELATIVE R[2]C[1]" to A1-style address', () => {
 | 
			
		||||
    const start = 'A1'
 | 
			
		||||
    const finish = 'RELATIVE R[2]C[1]'
 | 
			
		||||
    const result = getFinishingCell(start, finish, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('B3')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "MATCH A R[0]C[1]:valueA1" to A1-style address', () => {
 | 
			
		||||
    const start = 'A1'
 | 
			
		||||
    const finish = 'MATCH A R[0]C[1]:valueA1'
 | 
			
		||||
    const result = getFinishingCell(start, finish, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('B1')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "MATCH 1 R[4]C[0]:valueB1" to A1-style address', () => {
 | 
			
		||||
    const start = 'A1'
 | 
			
		||||
    const finish = 'MATCH 1 R[4]C[0]:valueB1'
 | 
			
		||||
    const result = getFinishingCell(start, finish, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('B5')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "LASTDOWN" to A1-style address of the last non-blank cell in column A', () => {
 | 
			
		||||
    const start = 'A1'
 | 
			
		||||
    const finish = 'LASTDOWN'
 | 
			
		||||
    const result = getFinishingCell(start, finish, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('A3')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should convert "BLANKROW" to A1-style address of the last row with blank cells', () => {
 | 
			
		||||
    const start = 'A1'
 | 
			
		||||
    const finish = 'BLANKROW'
 | 
			
		||||
    const result = getFinishingCell(start, finish, arrayOfObjects)
 | 
			
		||||
    expect(result).toBe('B6')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										31
									
								
								client/src/app/xlmap/utils/file.utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								client/src/app/xlmap/utils/file.utils.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
export const blobToFile = (blob: Blob, fileName: string): File => {
 | 
			
		||||
  const file = new File([blob], fileName, {
 | 
			
		||||
    lastModified: new Date().getTime()
 | 
			
		||||
  })
 | 
			
		||||
  return file
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Convert an array of bytes (Uint8Array) to a binary string.
 | 
			
		||||
 * @param {Uint8Array} res - The array of bytes to convert.
 | 
			
		||||
 * @returns {string} The binary string representation of the array of bytes.
 | 
			
		||||
 */
 | 
			
		||||
export const byteArrayToBinaryString = (res: Uint8Array): string => {
 | 
			
		||||
  // Create a Uint8Array from the input array (if it's not already)
 | 
			
		||||
  const bytes = new Uint8Array(res)
 | 
			
		||||
 | 
			
		||||
  // Initialize an empty string to store the binary representation
 | 
			
		||||
  let binary = ''
 | 
			
		||||
 | 
			
		||||
  // Get the length of the byte array
 | 
			
		||||
  const length = bytes.byteLength
 | 
			
		||||
 | 
			
		||||
  // Iterate through each byte in the array
 | 
			
		||||
  for (let i = 0; i < length; i++) {
 | 
			
		||||
    // Convert each byte to its binary representation and append to the string
 | 
			
		||||
    binary += String.fromCharCode(bytes[i])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Return the binary string
 | 
			
		||||
  return binary
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										225
									
								
								client/src/app/xlmap/utils/xl.utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								client/src/app/xlmap/utils/xl.utils.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,225 @@
 | 
			
		||||
import * as XLSX from '@sheet/crypto'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Checks if an excel row is blank or not
 | 
			
		||||
 *
 | 
			
		||||
 * @param row object is of shape {[key: string]: any}
 | 
			
		||||
 */
 | 
			
		||||
export const isBlankRow = (row: any) => {
 | 
			
		||||
  for (const key in row) {
 | 
			
		||||
    if (key !== '__rowNum__') {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Extracts row and column number from xlmap rule.
 | 
			
		||||
 *
 | 
			
		||||
 * Input string should be in form of
 | 
			
		||||
 * either "MATCH F R[2]C[0]: CASH BALANCE" or "RELATIVE R[10]C[6]"
 | 
			
		||||
 */
 | 
			
		||||
export const extractRowAndCol = (str: string) => {
 | 
			
		||||
  // Regular expression to match and capture the values inside square brackets
 | 
			
		||||
  const regex = /R\[(\d+)\]C\[(\d+)\]/
 | 
			
		||||
 | 
			
		||||
  // Match the regular expression against the input string
 | 
			
		||||
  const match = str.match(regex)
 | 
			
		||||
 | 
			
		||||
  if (!match) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Extract values from the match groups
 | 
			
		||||
  const row = parseInt(match[1], 10)
 | 
			
		||||
  const column = parseInt(match[2], 10)
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    row,
 | 
			
		||||
    column
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generate an A1-Style excel cell address from xlmap rule.
 | 
			
		||||
 *
 | 
			
		||||
 * Expect "ABSOLUTE D8" or "RELATIVE R[10]C[6]" or
 | 
			
		||||
 * "MATCH C R[0]C[4]:Common Equity Tier 1 (CET1)" kinds of string as rule input
 | 
			
		||||
 */
 | 
			
		||||
export const getCellAddress = (rule: string, arrayOfObjects: any[]) => {
 | 
			
		||||
  if (rule.startsWith('ABSOLUTE ')) {
 | 
			
		||||
    rule = rule.replace('ABSOLUTE ', '')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rule.startsWith('RELATIVE ')) {
 | 
			
		||||
    const rowAndCol = extractRowAndCol(rule)
 | 
			
		||||
 | 
			
		||||
    if (rowAndCol) {
 | 
			
		||||
      const { row, column } = rowAndCol
 | 
			
		||||
 | 
			
		||||
      // Generate an A1-Style address string from a SheetJS cell address
 | 
			
		||||
      // Spreadsheet applications typically display ordinal row numbers,
 | 
			
		||||
      // where 1 is the first row, 2 is the second row, etc. The numbering starts at 1.
 | 
			
		||||
      // SheetJS follows JavaScript counting conventions,
 | 
			
		||||
      // where 0 is the first row, 1 is the second row, etc. The numbering starts at 0.
 | 
			
		||||
      // Therefore, we have to subtract 1 from row and column to match SheetJS indexing convention
 | 
			
		||||
      rule = XLSX.utils.encode_cell({ r: row - 1, c: column - 1 })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rule.startsWith('MATCH ')) {
 | 
			
		||||
    let targetValue = ''
 | 
			
		||||
 | 
			
		||||
    // using a regular expression to match "C[x]:" and extract the value after it
 | 
			
		||||
    const match = rule.match(/C\[\d+\]:(.+)/)
 | 
			
		||||
 | 
			
		||||
    // Check if there is a match
 | 
			
		||||
    if (match) {
 | 
			
		||||
      // Extract the value after "C[x]:"
 | 
			
		||||
      targetValue = match[1]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Split the string by spaces to get target row/column
 | 
			
		||||
    const splittedArray = rule.split(' ')
 | 
			
		||||
 | 
			
		||||
    // Extract the second word
 | 
			
		||||
    const secondWord = splittedArray[1]
 | 
			
		||||
 | 
			
		||||
    let targetColumn = ''
 | 
			
		||||
    let targetRow = -1
 | 
			
		||||
    let cellAddress = ''
 | 
			
		||||
 | 
			
		||||
    // Check if the secondWord is a number
 | 
			
		||||
    if (!isNaN(Number(secondWord))) {
 | 
			
		||||
      targetRow = parseInt(secondWord)
 | 
			
		||||
    } else {
 | 
			
		||||
      targetColumn = secondWord
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (targetRow !== -1) {
 | 
			
		||||
      // sheetJS index starts from 0,
 | 
			
		||||
      // therefore, decremented 1 to make it correct row address for js array
 | 
			
		||||
      const row = arrayOfObjects[targetRow - 1]
 | 
			
		||||
      for (const col in row) {
 | 
			
		||||
        if (col !== '__rowNum__' && row[col] === targetValue) {
 | 
			
		||||
          cellAddress = col + targetRow
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      for (let i = 0; i < arrayOfObjects.length; i++) {
 | 
			
		||||
        const row = arrayOfObjects[i]
 | 
			
		||||
        if (row[targetColumn] === targetValue) {
 | 
			
		||||
          // sheetJS index starts from 0,
 | 
			
		||||
          // therefore, incremented 1 to make it correct row address
 | 
			
		||||
          const rowIndex = i + 1
 | 
			
		||||
          cellAddress = targetColumn + rowIndex
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Converts A1 cell address to 0-indexed form
 | 
			
		||||
    const matchedCellAddress = XLSX.utils.decode_cell(cellAddress)
 | 
			
		||||
 | 
			
		||||
    // extract number of rows and columns that we have to move
 | 
			
		||||
    // from matched cell to reach target cell
 | 
			
		||||
    const rowAndCol = extractRowAndCol(rule)
 | 
			
		||||
 | 
			
		||||
    if (rowAndCol) {
 | 
			
		||||
      const { row, column } = rowAndCol
 | 
			
		||||
 | 
			
		||||
      // Converts 0-indexed cell address to A1 form
 | 
			
		||||
      rule = XLSX.utils.encode_cell({
 | 
			
		||||
        r: matchedCellAddress.r + row,
 | 
			
		||||
        c: matchedCellAddress.c + column
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return rule
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generate an A1-Style excel cell address for last cell
 | 
			
		||||
 *
 | 
			
		||||
 * @param start A1 style excel cell address
 | 
			
		||||
 * @param finish XLMAP_FINISH attribute of xlmap rule
 | 
			
		||||
 * @param arrayOfObjects an array of row objects
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
export const getFinishingCell = (
 | 
			
		||||
  start: string,
 | 
			
		||||
  finish: string,
 | 
			
		||||
  arrayOfObjects: any[]
 | 
			
		||||
) => {
 | 
			
		||||
  // in this case an individual cell would be extracted
 | 
			
		||||
  if (finish === '') {
 | 
			
		||||
    return start
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (finish.startsWith('ABSOLUTE ')) {
 | 
			
		||||
    finish = finish.replace('ABSOLUTE ', '')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (finish.startsWith('RELATIVE ')) {
 | 
			
		||||
    const rowAndCol = extractRowAndCol(finish)
 | 
			
		||||
    if (rowAndCol) {
 | 
			
		||||
      const { row, column } = rowAndCol
 | 
			
		||||
 | 
			
		||||
      const { r, c } = XLSX.utils.decode_cell(start)
 | 
			
		||||
 | 
			
		||||
      // finish is relative to starting point
 | 
			
		||||
      // therefore, we need to add extracted row and columns
 | 
			
		||||
      // in starting cell address to get actual finishing cell
 | 
			
		||||
      finish = XLSX.utils.encode_cell({ r: r + row, c: c + column })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (finish.startsWith('MATCH ')) {
 | 
			
		||||
    finish = getCellAddress(finish, arrayOfObjects)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (finish === 'LASTDOWN') {
 | 
			
		||||
    const { r, c } = XLSX.utils.decode_cell(start)
 | 
			
		||||
    const colName = XLSX.utils.encode_col(c)
 | 
			
		||||
    let lastNonBlank = r
 | 
			
		||||
    for (let i = r + 1; i < arrayOfObjects.length; i++) {
 | 
			
		||||
      const row = arrayOfObjects[i]
 | 
			
		||||
      if (!row[colName]) {
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
      lastNonBlank = i
 | 
			
		||||
    }
 | 
			
		||||
    finish = colName + (lastNonBlank + 1) // excel numbering starts from 1. So incremented 1 to 0 based index
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (finish === 'BLANKROW') {
 | 
			
		||||
    const { r } = XLSX.utils.decode_cell(start)
 | 
			
		||||
    let lastNonBlankRow = r
 | 
			
		||||
    for (let i = r + 1; i < arrayOfObjects.length; i++) {
 | 
			
		||||
      const row = arrayOfObjects[i]
 | 
			
		||||
      if (isBlankRow(row)) {
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
      lastNonBlankRow = i
 | 
			
		||||
    }
 | 
			
		||||
    const row = arrayOfObjects[lastNonBlankRow]
 | 
			
		||||
 | 
			
		||||
    // Get the keys of the object (excluding '__rowNum__')
 | 
			
		||||
    const keys = Object.keys(row).filter((key) => key !== '__rowNum__')
 | 
			
		||||
 | 
			
		||||
    // Finding last column in a row
 | 
			
		||||
    // Find the key with the highest alphanumeric value (assumes keys are letters)
 | 
			
		||||
    const lastColumn = keys.reduce(
 | 
			
		||||
      (maxKey, currentKey) => (currentKey > maxKey ? currentKey : maxKey),
 | 
			
		||||
      ''
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    // make finishing cell address in A1 style
 | 
			
		||||
    finish = lastColumn + (lastNonBlankRow + 1) // excel numbering starts from 1. So incremented 1 to 0 based index
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return finish
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								client/src/app/xlmap/xlmap-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								client/src/app/xlmap/xlmap-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
import { NgModule } from '@angular/core'
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router'
 | 
			
		||||
 | 
			
		||||
import { XLMapComponent } from '../xlmap/xlmap.component'
 | 
			
		||||
import { XLMapRouteComponent } from '../routes/xlmap-route/xlmap-route.component'
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: '',
 | 
			
		||||
    component: XLMapRouteComponent,
 | 
			
		||||
    children: [
 | 
			
		||||
      { path: '', component: XLMapComponent },
 | 
			
		||||
      { path: ':id', component: XLMapComponent }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  imports: [RouterModule.forChild(routes)],
 | 
			
		||||
  exports: [RouterModule]
 | 
			
		||||
})
 | 
			
		||||
export class XLMapRoutingModule {}
 | 
			
		||||
							
								
								
									
										260
									
								
								client/src/app/xlmap/xlmap.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								client/src/app/xlmap/xlmap.component.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,260 @@
 | 
			
		||||
<app-sidebar>
 | 
			
		||||
  <div *ngIf="xlmapsLoading" class="my-10-mx-auto text-center">
 | 
			
		||||
    <clr-spinner clrMedium></clr-spinner>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <clr-tree>
 | 
			
		||||
    <clr-tree-node class="search-node">
 | 
			
		||||
      <div class="tree-search-wrapper">
 | 
			
		||||
        <input
 | 
			
		||||
          clrInput
 | 
			
		||||
          #searchXLMapTreeInput
 | 
			
		||||
          placeholder="Filter by Id"
 | 
			
		||||
          name="input"
 | 
			
		||||
          [(ngModel)]="searchString"
 | 
			
		||||
          (keyup)="xlmapListOnFilter()"
 | 
			
		||||
          autocomplete="off"
 | 
			
		||||
        />
 | 
			
		||||
        <clr-icon
 | 
			
		||||
          *ngIf="searchXLMapTreeInput.value.length < 1"
 | 
			
		||||
          shape="search"
 | 
			
		||||
        ></clr-icon>
 | 
			
		||||
        <clr-icon
 | 
			
		||||
          *ngIf="searchXLMapTreeInput.value.length > 0"
 | 
			
		||||
          (click)="searchString = ''; xlmapListOnFilter()"
 | 
			
		||||
          shape="times"
 | 
			
		||||
        ></clr-icon>
 | 
			
		||||
      </div>
 | 
			
		||||
    </clr-tree-node>
 | 
			
		||||
 | 
			
		||||
    <ng-container *ngFor="let xlmap of xlmaps">
 | 
			
		||||
      <clr-tree-node>
 | 
			
		||||
        <button
 | 
			
		||||
          (click)="xlmapOnClick(xlmap)"
 | 
			
		||||
          class="clr-treenode-link"
 | 
			
		||||
          [class.table-active]="isActiveXLMap(xlmap.id)"
 | 
			
		||||
        >
 | 
			
		||||
          <clr-icon shape="file"></clr-icon>
 | 
			
		||||
          {{ xlmap.id }}
 | 
			
		||||
        </button>
 | 
			
		||||
      </clr-tree-node>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
  </clr-tree>
 | 
			
		||||
</app-sidebar>
 | 
			
		||||
 | 
			
		||||
<div class="content-area">
 | 
			
		||||
  <div *ngIf="!selectedXLMap" class="no-table-selected">
 | 
			
		||||
    <clr-icon
 | 
			
		||||
      shape="warning-standard"
 | 
			
		||||
      size="60"
 | 
			
		||||
      class="is-info icon-dc-fill"
 | 
			
		||||
    ></clr-icon>
 | 
			
		||||
    <h3 *ngIf="xlmaps.length > 0" class="text-center color-gray">
 | 
			
		||||
      Please select a map
 | 
			
		||||
    </h3>
 | 
			
		||||
    <h3 *ngIf="xlmaps.length < 1" class="text-center color-gray">
 | 
			
		||||
      No excel map is found
 | 
			
		||||
    </h3>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="loadingSpinner" *ngIf="isLoading">
 | 
			
		||||
    <span class="spinner"> Loading... </span>
 | 
			
		||||
    <div>
 | 
			
		||||
      <h4>{{ isLoadingDesc }}</h4>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div
 | 
			
		||||
    appDragNdrop
 | 
			
		||||
    (fileDraggedOver)="onShowUploadModal()"
 | 
			
		||||
    class="card h-100 d-flex clr-flex-column"
 | 
			
		||||
    *ngIf="!isLoading && selectedXLMap"
 | 
			
		||||
  >
 | 
			
		||||
    <clr-tabs>
 | 
			
		||||
      <clr-tab>
 | 
			
		||||
        <button clrTabLink (click)="selectedTab = TabsEnum.Rules">Rules</button>
 | 
			
		||||
        <clr-tab-content *clrIfActive="selectedTab === TabsEnum.Rules">
 | 
			
		||||
        </clr-tab-content>
 | 
			
		||||
      </clr-tab>
 | 
			
		||||
      <clr-tab>
 | 
			
		||||
        <button clrTabLink (click)="selectedTab = TabsEnum.Data">Data</button>
 | 
			
		||||
        <clr-tab-content *clrIfActive="selectedTab === TabsEnum.Data">
 | 
			
		||||
        </clr-tab-content>
 | 
			
		||||
      </clr-tab>
 | 
			
		||||
    </clr-tabs>
 | 
			
		||||
 | 
			
		||||
    <ng-container *ngTemplateOutlet="actionButtons"></ng-container>
 | 
			
		||||
 | 
			
		||||
    <div class="clr-row m-0 mb-10-i viewerTitle">
 | 
			
		||||
      <h3 class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
 | 
			
		||||
        {{ selectedXLMap.id }}
 | 
			
		||||
      </h3>
 | 
			
		||||
      <i class="d-flex clr-col-12 clr-justify-content-center mt-5-i">{{
 | 
			
		||||
        selectedXLMap.description
 | 
			
		||||
      }}</i>
 | 
			
		||||
      <h5 class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
 | 
			
		||||
        Rules Source:
 | 
			
		||||
        <a
 | 
			
		||||
          cds-text="labelLink"
 | 
			
		||||
          class="ml-10"
 | 
			
		||||
          [routerLink]="'/view/data/' + rulesSource"
 | 
			
		||||
        >
 | 
			
		||||
          {{ rulesSource }}
 | 
			
		||||
        </a>
 | 
			
		||||
      </h5>
 | 
			
		||||
      <h5 class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
 | 
			
		||||
        Target dataset:
 | 
			
		||||
        <a
 | 
			
		||||
          cds-text="labelLink"
 | 
			
		||||
          class="ml-10"
 | 
			
		||||
          [routerLink]="'/view/data/' + selectedXLMap.targetDS"
 | 
			
		||||
        >
 | 
			
		||||
          {{ selectedXLMap.targetDS }}
 | 
			
		||||
        </a>
 | 
			
		||||
      </h5>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="clr-flex-1">
 | 
			
		||||
      <hot-table
 | 
			
		||||
        hotId="hotInstance"
 | 
			
		||||
        id="hot-table"
 | 
			
		||||
        [multiColumnSorting]="true"
 | 
			
		||||
        [viewportRowRenderingOffset]="50"
 | 
			
		||||
        [data]="selectedTab === TabsEnum.Rules ? xlmapRules : xlData"
 | 
			
		||||
        [colHeaders]="
 | 
			
		||||
          selectedTab === TabsEnum.Rules ? xlmapRulesHeaders : xlUploadHeader
 | 
			
		||||
        "
 | 
			
		||||
        [columns]="
 | 
			
		||||
          selectedTab === TabsEnum.Rules ? xlmapRulesColumns : xlUploadColumns
 | 
			
		||||
        "
 | 
			
		||||
        [filters]="true"
 | 
			
		||||
        [height]="'100%'"
 | 
			
		||||
        stretchH="all"
 | 
			
		||||
        [modifyColWidth]="maxWidthChecker"
 | 
			
		||||
        [cells]="getCellConfiguration"
 | 
			
		||||
        [maxRows]="hotTableMaxRows"
 | 
			
		||||
        [manualColumnResize]="true"
 | 
			
		||||
        [rowHeaders]="rowHeaders"
 | 
			
		||||
        [rowHeaderWidth]="15"
 | 
			
		||||
        [rowHeights]="20"
 | 
			
		||||
        [licenseKey]="hotTableLicenseKey"
 | 
			
		||||
      >
 | 
			
		||||
      </hot-table>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <clr-modal
 | 
			
		||||
    appFileDrop
 | 
			
		||||
    (fileOver)="fileOverBase($event)"
 | 
			
		||||
    (fileDrop)="getFileDesc($event, true)"
 | 
			
		||||
    [uploader]="uploader"
 | 
			
		||||
    [clrModalSize]="'xl'"
 | 
			
		||||
    [clrModalStaticBackdrop]="false"
 | 
			
		||||
    [clrModalClosable]="true"
 | 
			
		||||
    [(clrModalOpen)]="showUploadModal"
 | 
			
		||||
    class="relative"
 | 
			
		||||
  >
 | 
			
		||||
    <h3 class="modal-title">Upload File</h3>
 | 
			
		||||
    <div class="modal-body">
 | 
			
		||||
      <div class="drop-area">
 | 
			
		||||
        <span>Drop file anywhere to upload!</span>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="clr-col-md-12">
 | 
			
		||||
        <div class="clr-row card-block mt-15 d-flex justify-content-between">
 | 
			
		||||
          <div class="clr-col-md-3 filterBtn">
 | 
			
		||||
            <span class="filterBtn w-100">
 | 
			
		||||
              <label
 | 
			
		||||
                for="file-upload"
 | 
			
		||||
                class="btn btn-sm btn-outline profile-buttons w-100"
 | 
			
		||||
              >
 | 
			
		||||
                Browse
 | 
			
		||||
              </label>
 | 
			
		||||
            </span>
 | 
			
		||||
            <input
 | 
			
		||||
              hidden
 | 
			
		||||
              #fileUploadInput
 | 
			
		||||
              id="file-upload"
 | 
			
		||||
              type="file"
 | 
			
		||||
              appFileSelect
 | 
			
		||||
              [uploader]="uploader"
 | 
			
		||||
              (change)="getFileDesc($event)"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </clr-modal>
 | 
			
		||||
 | 
			
		||||
  <clr-modal [(clrModalOpen)]="submitLimitNotice">
 | 
			
		||||
    <h3 class="modal-title">Notice</h3>
 | 
			
		||||
    <div class="modal-body">
 | 
			
		||||
      <p class="m-0">
 | 
			
		||||
        Due to current licence, only
 | 
			
		||||
        {{ licenceState.value.submit_rows_limit }} rows in a file will be
 | 
			
		||||
        submitted. To remove the restriction, contact
 | 
			
		||||
        support@datacontroller.io
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="modal-footer">
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        class="btn btn-sm btn-primary"
 | 
			
		||||
        (click)="submitLimitNotice = false"
 | 
			
		||||
      >
 | 
			
		||||
        Cancel
 | 
			
		||||
      </button>
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        class="btn btn-sm btn-primary"
 | 
			
		||||
        (click)="submit(); submitLimitNotice = false"
 | 
			
		||||
      >
 | 
			
		||||
        Submit
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </clr-modal>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #actionButtons>
 | 
			
		||||
  <div class="clr-row m-0 clr-justify-content-center">
 | 
			
		||||
    <div
 | 
			
		||||
      *ngIf="status === StatusEnum.ReadyToUpload"
 | 
			
		||||
      class="d-flex clr-justify-content-center clr-col-12 clr-col-lg-4"
 | 
			
		||||
    >
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        class="btn btn-sm btn-success btn-block mr-0"
 | 
			
		||||
        (click)="onShowUploadModal()"
 | 
			
		||||
      >
 | 
			
		||||
        <clr-icon shape="upload"></clr-icon>
 | 
			
		||||
        <span>Upload</span>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
      *ngIf="status === StatusEnum.ReadyToSubmit"
 | 
			
		||||
      class="d-flex clr-justify-content-center clr-col-12 clr-col-lg-4"
 | 
			
		||||
    >
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        class="btn btn-sm btn-success btn-block mr-0"
 | 
			
		||||
        (click)="submitExcel()"
 | 
			
		||||
      >
 | 
			
		||||
        <clr-icon shape="upload"></clr-icon>
 | 
			
		||||
        <span>Submit</span>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
      *ngIf="status === StatusEnum.ReadyToSubmit"
 | 
			
		||||
      class="d-flex clr-justify-content-center clr-col-12 clr-col-lg-4"
 | 
			
		||||
    >
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        class="btn btn-sm btn-outline-danger btn-block mr-0"
 | 
			
		||||
        (click)="discardExtractedData()"
 | 
			
		||||
      >
 | 
			
		||||
        <clr-icon shape="times"></clr-icon>
 | 
			
		||||
        <span>Discard</span>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
							
								
								
									
										77
									
								
								client/src/app/xlmap/xlmap.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								client/src/app/xlmap/xlmap.component.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
.card {
 | 
			
		||||
  margin-top: 0;
 | 
			
		||||
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
clr-tree-node button {
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.no-table-selected {
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header-row {
 | 
			
		||||
  .title-col {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
  .options-col {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sw {
 | 
			
		||||
  margin: 1rem 0rem 0.5rem 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.viewerTitle {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.cardFlex {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content-area {
 | 
			
		||||
  padding: 0.5rem !important;
 | 
			
		||||
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
hot-table {
 | 
			
		||||
  ::ng-deep {
 | 
			
		||||
    .primaryKeyHeaderStyle {
 | 
			
		||||
      background: #306b006e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.drop-area {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
 | 
			
		||||
  margin: 1px;
 | 
			
		||||
 | 
			
		||||
  border: 2px dashed #fff;
 | 
			
		||||
 | 
			
		||||
  z-index: -1;
 | 
			
		||||
 | 
			
		||||
  span {
 | 
			
		||||
    font-size: 20px;
 | 
			
		||||
    margin-top: 20px;
 | 
			
		||||
    color: #fff;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										490
									
								
								client/src/app/xlmap/xlmap.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										490
									
								
								client/src/app/xlmap/xlmap.component.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,490 @@
 | 
			
		||||
import {
 | 
			
		||||
  AfterContentInit,
 | 
			
		||||
  AfterViewInit,
 | 
			
		||||
  Component,
 | 
			
		||||
  ElementRef,
 | 
			
		||||
  HostBinding,
 | 
			
		||||
  OnInit,
 | 
			
		||||
  QueryList,
 | 
			
		||||
  ViewChildren
 | 
			
		||||
} from '@angular/core'
 | 
			
		||||
import { ActivatedRoute, Router } from '@angular/router'
 | 
			
		||||
import { UploadFile } from '@sasjs/adapter'
 | 
			
		||||
import * as XLSX from '@sheet/crypto'
 | 
			
		||||
import { XLMapListItem, globals } from '../_globals'
 | 
			
		||||
import { FileUploader } from '../models/FileUploader.class'
 | 
			
		||||
import {
 | 
			
		||||
  EventService,
 | 
			
		||||
  LicenceService,
 | 
			
		||||
  LoggerService,
 | 
			
		||||
  SasService,
 | 
			
		||||
  SasStoreService
 | 
			
		||||
} from '../services'
 | 
			
		||||
import { getCellAddress, getFinishingCell } from './utils/xl.utils'
 | 
			
		||||
import { blobToFile, byteArrayToBinaryString } from './utils/file.utils'
 | 
			
		||||
 | 
			
		||||
interface XLMapRule {
 | 
			
		||||
  XLMAP_ID: string
 | 
			
		||||
  XLMAP_SHEET: string
 | 
			
		||||
  XLMAP_RANGE_ID: string
 | 
			
		||||
  XLMAP_START: string
 | 
			
		||||
  XLMAP_FINISH: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface XLUploadEntry {
 | 
			
		||||
  LOAD_REF: string
 | 
			
		||||
  XLMAP_ID: string
 | 
			
		||||
  XLMAP_RANGE_ID: string
 | 
			
		||||
  ROW_NO: number
 | 
			
		||||
  COL_NO: number
 | 
			
		||||
  VALUE_TXT: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum Status {
 | 
			
		||||
  NoMapSelected,
 | 
			
		||||
  FetchingRules,
 | 
			
		||||
  ReadyToUpload,
 | 
			
		||||
  ExtractingData,
 | 
			
		||||
  ReadyToSubmit,
 | 
			
		||||
  SubmittingExtractedData,
 | 
			
		||||
  Submitting
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum Tabs {
 | 
			
		||||
  Rules,
 | 
			
		||||
  Data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-xlmap',
 | 
			
		||||
  templateUrl: './xlmap.component.html',
 | 
			
		||||
  styleUrls: ['./xlmap.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
 | 
			
		||||
  @HostBinding('class.content-container') contentContainerClass = true
 | 
			
		||||
  @ViewChildren('fileUploadInput')
 | 
			
		||||
  fileUploadInputCompList: QueryList<ElementRef> = new QueryList()
 | 
			
		||||
 | 
			
		||||
  StatusEnum = Status
 | 
			
		||||
  TabsEnum = Tabs
 | 
			
		||||
 | 
			
		||||
  public selectedTab = Tabs.Rules
 | 
			
		||||
  public rulesSource = globals.dcLib + '.MPE_XLMAP_RULES'
 | 
			
		||||
 | 
			
		||||
  public xlmaps: XLMapListItem[] = []
 | 
			
		||||
  public selectedXLMap: XLMapListItem | undefined = undefined
 | 
			
		||||
  public searchString = ''
 | 
			
		||||
  public xlmapsLoading = true
 | 
			
		||||
  public isLoading = false
 | 
			
		||||
  public isLoadingDesc = ''
 | 
			
		||||
  public status = Status.NoMapSelected
 | 
			
		||||
 | 
			
		||||
  public xlmapRulesHeaders = [
 | 
			
		||||
    'XLMAP_SHEET',
 | 
			
		||||
    'XLMAP_RANGE_ID',
 | 
			
		||||
    'XLMAP_START',
 | 
			
		||||
    'XLMAP_FINISH'
 | 
			
		||||
  ]
 | 
			
		||||
  public xlmapRulesColumns = [
 | 
			
		||||
    {
 | 
			
		||||
      data: 'XLMAP_SHEET'
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      data: 'XLMAP_RANGE_ID'
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      data: 'XLMAP_START'
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      data: 'XLMAP_FINISH'
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
  public xlmapRules: XLMapRule[] = []
 | 
			
		||||
 | 
			
		||||
  public xlUploadHeader = ['XLMAP_RANGE_ID', 'ROW_NO', 'COL_NO', 'VALUE_TXT']
 | 
			
		||||
  public xlUploadColumns = [
 | 
			
		||||
    {
 | 
			
		||||
      data: 'XLMAP_RANGE_ID'
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      data: 'ROW_NO'
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      data: 'COL_NO'
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      data: 'VALUE_TXT'
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
  public xlData: XLUploadEntry[] = []
 | 
			
		||||
 | 
			
		||||
  public showUploadModal = false
 | 
			
		||||
  public hasBaseDropZoneOver = false
 | 
			
		||||
  public filename = ''
 | 
			
		||||
  public submitLimitNotice = false
 | 
			
		||||
 | 
			
		||||
  public uploader: FileUploader = new FileUploader()
 | 
			
		||||
 | 
			
		||||
  public licenceState = this.licenceService.licenceState
 | 
			
		||||
 | 
			
		||||
  public hotTableLicenseKey: string | undefined = undefined
 | 
			
		||||
  public hotTableMaxRows =
 | 
			
		||||
    this.licenceState.value.viewer_rows_allowed || Infinity
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private eventService: EventService,
 | 
			
		||||
    private licenceService: LicenceService,
 | 
			
		||||
    private loggerService: LoggerService,
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private sasStoreService: SasStoreService,
 | 
			
		||||
    private sasService: SasService
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  public xlmapOnClick(xlmap: XLMapListItem) {
 | 
			
		||||
    if (xlmap.id !== this.selectedXLMap?.id) {
 | 
			
		||||
      this.selectedXLMap = xlmap
 | 
			
		||||
      this.xlData = []
 | 
			
		||||
      this.filename = ''
 | 
			
		||||
      this.uploader.queue = []
 | 
			
		||||
      if (this.fileUploadInputCompList.first) {
 | 
			
		||||
        this.fileUploadInputCompList.first.nativeElement.value = ''
 | 
			
		||||
      }
 | 
			
		||||
      this.selectedTab = Tabs.Rules
 | 
			
		||||
      this.viewXLMapRules()
 | 
			
		||||
      this.router.navigateByUrl('/home/files/' + xlmap.id)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public xlmapListOnFilter() {
 | 
			
		||||
    if (this.searchString.length > 0) {
 | 
			
		||||
      const array: XLMapListItem[] = globals.xlmaps
 | 
			
		||||
      this.xlmaps = array.filter((item) =>
 | 
			
		||||
        item.id.toLowerCase().includes(this.searchString.toLowerCase())
 | 
			
		||||
      )
 | 
			
		||||
    } else {
 | 
			
		||||
      this.xlmaps = globals.xlmaps
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public isActiveXLMap(id: string) {
 | 
			
		||||
    return this.selectedXLMap?.id === id
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public maxWidthChecker(width: any, col: any) {
 | 
			
		||||
    if (width > 200) return 200
 | 
			
		||||
    else return width
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getCellConfiguration() {
 | 
			
		||||
    return { readOnly: true }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public rowHeaders() {
 | 
			
		||||
    return ' '
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public onShowUploadModal() {
 | 
			
		||||
    this.showUploadModal = true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Called by FileDropDirective
 | 
			
		||||
   * @param e true if file is dragged over the drop zone
 | 
			
		||||
   */
 | 
			
		||||
  public fileOverBase(e: boolean): void {
 | 
			
		||||
    this.hasBaseDropZoneOver = e
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getFileDesc(event: any, dropped = false) {
 | 
			
		||||
    const file = dropped ? event[0] : event.target.files[0]
 | 
			
		||||
 | 
			
		||||
    if (!file) return
 | 
			
		||||
 | 
			
		||||
    const filename = file.name
 | 
			
		||||
    this.filename = filename
 | 
			
		||||
 | 
			
		||||
    const fileType = filename.slice(
 | 
			
		||||
      filename.lastIndexOf('.') + 1,
 | 
			
		||||
      filename.lastIndexOf('.') + 4
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if (fileType.toLowerCase() === 'xls') {
 | 
			
		||||
      this.showUploadModal = false
 | 
			
		||||
      this.isLoading = true
 | 
			
		||||
      this.isLoadingDesc = 'Extracting Data'
 | 
			
		||||
      this.status = Status.ExtractingData
 | 
			
		||||
 | 
			
		||||
      const reader = new FileReader()
 | 
			
		||||
      reader.onload = async (theFile: any) => {
 | 
			
		||||
        /* read workbook */
 | 
			
		||||
        const bstr = byteArrayToBinaryString(theFile.target.result)
 | 
			
		||||
        let wb: XLSX.WorkBook | undefined = undefined
 | 
			
		||||
 | 
			
		||||
        const xlsxOptions: XLSX.ParsingOptions = {
 | 
			
		||||
          type: 'binary',
 | 
			
		||||
          cellDates: false,
 | 
			
		||||
          cellFormula: true,
 | 
			
		||||
          cellStyles: true,
 | 
			
		||||
          cellNF: false,
 | 
			
		||||
          cellText: false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
          wb = XLSX.read(bstr, {
 | 
			
		||||
            ...xlsxOptions
 | 
			
		||||
          })
 | 
			
		||||
        } catch (err: any) {
 | 
			
		||||
          this.eventService.showAbortModal(
 | 
			
		||||
            null,
 | 
			
		||||
            err,
 | 
			
		||||
            undefined,
 | 
			
		||||
            'Error reading file'
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!wb) {
 | 
			
		||||
          this.isLoading = false
 | 
			
		||||
          this.isLoadingDesc = ''
 | 
			
		||||
          this.status = Status.ReadyToUpload
 | 
			
		||||
          this.uploader.queue.pop()
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.extractData(wb)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      reader.readAsArrayBuffer(file)
 | 
			
		||||
    } else {
 | 
			
		||||
      this.isLoading = false
 | 
			
		||||
      this.isLoadingDesc = ''
 | 
			
		||||
      this.status = Status.ReadyToUpload
 | 
			
		||||
      this.showUploadModal = true
 | 
			
		||||
      this.uploader.queue.pop()
 | 
			
		||||
 | 
			
		||||
      const abortMsg =
 | 
			
		||||
        'Invalid file type "<b>' +
 | 
			
		||||
        this.filename +
 | 
			
		||||
        '</b>". Please upload excel file.'
 | 
			
		||||
      this.eventService.showAbortModal(null, abortMsg)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public discardExtractedData() {
 | 
			
		||||
    this.isLoading = false
 | 
			
		||||
    this.isLoadingDesc = ''
 | 
			
		||||
    this.status = Status.ReadyToUpload
 | 
			
		||||
    this.xlData = []
 | 
			
		||||
    this.selectedTab = Tabs.Rules
 | 
			
		||||
    this.filename = ''
 | 
			
		||||
    this.uploader.queue = []
 | 
			
		||||
    if (this.fileUploadInputCompList.first) {
 | 
			
		||||
      this.fileUploadInputCompList.first.nativeElement.value = ''
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Submits attached excel file that is in preview mode
 | 
			
		||||
   */
 | 
			
		||||
  public submitExcel() {
 | 
			
		||||
    if (this.licenceState.value.submit_rows_limit !== Infinity) {
 | 
			
		||||
      this.submitLimitNotice = true
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.submit()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public submit() {
 | 
			
		||||
    if (!this.selectedXLMap || !this.xlData.length) return
 | 
			
		||||
 | 
			
		||||
    this.status = Status.Submitting
 | 
			
		||||
    this.isLoading = true
 | 
			
		||||
    this.isLoadingDesc = 'Submitting extracted data'
 | 
			
		||||
 | 
			
		||||
    const filesToUpload: UploadFile[] = []
 | 
			
		||||
 | 
			
		||||
    for (const file of this.uploader.queue) {
 | 
			
		||||
      filesToUpload.push({
 | 
			
		||||
        file: file,
 | 
			
		||||
        fileName: file.name
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const csvContent =
 | 
			
		||||
      Object.keys(this.xlData[0]).join(',') +
 | 
			
		||||
      '\n' +
 | 
			
		||||
      this.xlData
 | 
			
		||||
        .slice(0, this.licenceState.value.submit_rows_limit)
 | 
			
		||||
        .map((row: any) => Object.values(row).join(','))
 | 
			
		||||
        .join('\n')
 | 
			
		||||
 | 
			
		||||
    const blob = new Blob([csvContent], { type: 'application/csv' })
 | 
			
		||||
    const file: File = blobToFile(blob, this.filename + '.csv')
 | 
			
		||||
 | 
			
		||||
    filesToUpload.push({
 | 
			
		||||
      file: file,
 | 
			
		||||
      fileName: file.name
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const uploadUrl = 'services/editors/loadfile'
 | 
			
		||||
    this.sasService
 | 
			
		||||
      .uploadFile(uploadUrl, filesToUpload, {
 | 
			
		||||
        table: this.selectedXLMap.targetDS
 | 
			
		||||
      })
 | 
			
		||||
      .then((res: any) => {
 | 
			
		||||
        if (res.sasjsAbort) {
 | 
			
		||||
          const abortRes = res
 | 
			
		||||
          const abortMsg = abortRes.sasjsAbort[0].MSG
 | 
			
		||||
          const macMsg = abortRes.sasjsAbort[0].MAC
 | 
			
		||||
 | 
			
		||||
          this.eventService.showAbortModal('', abortMsg, {
 | 
			
		||||
            SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
 | 
			
		||||
            SYSERRORTEXT: abortRes.SYSERRORTEXT,
 | 
			
		||||
            MAC: macMsg
 | 
			
		||||
          })
 | 
			
		||||
        } else if (res.sasparams) {
 | 
			
		||||
          const params = res.sasparams[0]
 | 
			
		||||
          const tableId = params.DSID
 | 
			
		||||
          this.router.navigateByUrl('/stage/' + tableId)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err: any) => {
 | 
			
		||||
        this.eventService.catchResponseError('file upload', err)
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => {
 | 
			
		||||
        this.status = Status.ReadyToSubmit
 | 
			
		||||
        this.isLoading = false
 | 
			
		||||
        this.isLoadingDesc = ''
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public extractData(wb: XLSX.WorkBook) {
 | 
			
		||||
    const extractedData: XLUploadEntry[] = []
 | 
			
		||||
 | 
			
		||||
    this.xlmapRules.forEach((rule) => {
 | 
			
		||||
      let sheetName = rule.XLMAP_SHEET
 | 
			
		||||
      // if sheet name is not an absolute name rather an index string like /1, /2, etc
 | 
			
		||||
      // we extract the index and find absolute sheet name for specified index
 | 
			
		||||
      if (sheetName.startsWith('/')) {
 | 
			
		||||
        const temp = sheetName.split('/')[1]
 | 
			
		||||
        const sheetIndex = parseInt(temp) - 1
 | 
			
		||||
        sheetName = wb.SheetNames[sheetIndex]
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const sheet = wb.Sheets[sheetName]
 | 
			
		||||
 | 
			
		||||
      const arrayOfObjects = <any[]>XLSX.utils.sheet_to_json(sheet, {
 | 
			
		||||
        raw: true,
 | 
			
		||||
        header: 'A',
 | 
			
		||||
        blankrows: true
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const start = getCellAddress(rule.XLMAP_START, arrayOfObjects)
 | 
			
		||||
      const finish = getFinishingCell(start, rule.XLMAP_FINISH, arrayOfObjects)
 | 
			
		||||
 | 
			
		||||
      const a1Range = `${start}:${finish}`
 | 
			
		||||
 | 
			
		||||
      const range = XLSX.utils.decode_range(a1Range)
 | 
			
		||||
 | 
			
		||||
      const rangedData = <any[]>XLSX.utils.sheet_to_json(sheet, {
 | 
			
		||||
        raw: true,
 | 
			
		||||
        range: a1Range,
 | 
			
		||||
        header: 'A',
 | 
			
		||||
        blankrows: true
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      for (let i = 0; i < rangedData.length; i++) {
 | 
			
		||||
        const row = rangedData[i]
 | 
			
		||||
 | 
			
		||||
        // `range.s.c` is the index of first column in the range
 | 
			
		||||
        // `range.e.c` is the index of last column in the range
 | 
			
		||||
        // we'll iterate from first column to last column and
 | 
			
		||||
        // extract value where defined and push to extracted data array
 | 
			
		||||
        for (let j = range.s.c, x = 0; j <= range.e.c; j++, x++) {
 | 
			
		||||
          const col = XLSX.utils.encode_col(j)
 | 
			
		||||
 | 
			
		||||
          if (col in row) {
 | 
			
		||||
            // in excel's R1C1 notation indexing starts from 1 but in JS it starts from 0
 | 
			
		||||
            // therefore, we'll have to add 1 to rows and cols
 | 
			
		||||
            extractedData.push({
 | 
			
		||||
              LOAD_REF: '0',
 | 
			
		||||
              XLMAP_ID: rule.XLMAP_ID,
 | 
			
		||||
              XLMAP_RANGE_ID: rule.XLMAP_RANGE_ID,
 | 
			
		||||
              ROW_NO: i + 1,
 | 
			
		||||
              COL_NO: x + 1,
 | 
			
		||||
              VALUE_TXT: row[col]
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.status = Status.ReadyToSubmit
 | 
			
		||||
    this.isLoading = false
 | 
			
		||||
    this.isLoadingDesc = ''
 | 
			
		||||
 | 
			
		||||
    this.xlData = extractedData
 | 
			
		||||
    this.selectedTab = Tabs.Data
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async viewXLMapRules() {
 | 
			
		||||
    if (!this.selectedXLMap) return
 | 
			
		||||
 | 
			
		||||
    this.isLoading = true
 | 
			
		||||
    this.isLoadingDesc = 'Loading excel rules'
 | 
			
		||||
    this.status = Status.FetchingRules
 | 
			
		||||
 | 
			
		||||
    await this.sasStoreService
 | 
			
		||||
      .getXLMapRules(this.selectedXLMap.id)
 | 
			
		||||
      .then((res) => {
 | 
			
		||||
        this.xlmapRules = res.xlmaprules
 | 
			
		||||
        this.status = Status.ReadyToUpload
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        this.loggerService.error(err)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    this.isLoading = false
 | 
			
		||||
    this.isLoadingDesc = ''
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private load() {
 | 
			
		||||
    this.xlmaps = globals.xlmaps
 | 
			
		||||
    this.xlmapsLoading = false
 | 
			
		||||
 | 
			
		||||
    const id = this.route.snapshot.params['id']
 | 
			
		||||
 | 
			
		||||
    if (id) {
 | 
			
		||||
      const xlmapListItem = this.xlmaps.find((item) => item.id === id)
 | 
			
		||||
      if (xlmapListItem) {
 | 
			
		||||
        this.selectedXLMap = xlmapListItem
 | 
			
		||||
        this.viewXLMapRules()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.licenceService.hot_license_key.subscribe(
 | 
			
		||||
      (hot_license_key: string | undefined) => {
 | 
			
		||||
        this.hotTableLicenseKey = hot_license_key
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit() {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngAfterContentInit(): void {
 | 
			
		||||
    if (globals.editor.startupSet) {
 | 
			
		||||
      this.load()
 | 
			
		||||
    } else {
 | 
			
		||||
      this.eventService.onStartupDataLoaded.subscribe(() => {
 | 
			
		||||
        this.load()
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								client/src/app/xlmap/xlmap.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								client/src/app/xlmap/xlmap.module.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
import { CommonModule } from '@angular/common'
 | 
			
		||||
import { NgModule } from '@angular/core'
 | 
			
		||||
import { FormsModule } from '@angular/forms'
 | 
			
		||||
import { ClarityModule } from '@clr/angular'
 | 
			
		||||
import { HotTableModule } from '@handsontable/angular'
 | 
			
		||||
import { registerAllModules } from 'handsontable/registry'
 | 
			
		||||
import { AppSharedModule } from '../app-shared.module'
 | 
			
		||||
import { DirectivesModule } from '../directives/directives.module'
 | 
			
		||||
import { XLMapRouteComponent } from '../routes/xlmap-route/xlmap-route.component'
 | 
			
		||||
import { DcTreeModule } from '../shared/dc-tree/dc-tree.module'
 | 
			
		||||
import { XLMapRoutingModule } from './xlmap-routing.module'
 | 
			
		||||
import { XLMapComponent } from './xlmap.component'
 | 
			
		||||
 | 
			
		||||
// register Handsontable's modules
 | 
			
		||||
registerAllModules()
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [XLMapRouteComponent, XLMapComponent],
 | 
			
		||||
  imports: [
 | 
			
		||||
    HotTableModule,
 | 
			
		||||
    XLMapRoutingModule,
 | 
			
		||||
    FormsModule,
 | 
			
		||||
    ClarityModule,
 | 
			
		||||
    AppSharedModule,
 | 
			
		||||
    CommonModule,
 | 
			
		||||
    DcTreeModule,
 | 
			
		||||
    DirectivesModule
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [XLMapComponent]
 | 
			
		||||
})
 | 
			
		||||
export class XLMapModule {}
 | 
			
		||||
@@ -15,7 +15,7 @@ try {
 | 
			
		||||
 | 
			
		||||
  writeFileSync(
 | 
			
		||||
    file,
 | 
			
		||||
    `//IMPORTANT: THIS FILE IS AUTO GENERATED BASED ON LICENCE.MD FILE!\nexport const EULA = \`\n${licence}\n\``,
 | 
			
		||||
    `//IMPORTANT: THIS FILE IS AUTO GENERATED BASED ON LICENCE.MD FILE!\nexport const EULA = \`\n${licence}\n\`\n`,
 | 
			
		||||
    { encoding: 'utf-8' }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,7 @@
 | 
			
		||||
  >
 | 
			
		||||
  </sasjs>
 | 
			
		||||
 | 
			
		||||
  <body class="m-0">
 | 
			
		||||
  <body cds-theme="light" class="m-0">
 | 
			
		||||
    <my-app></my-app>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,20 @@
 | 
			
		||||
/* You can add global styles to this file, and also import other style files */
 | 
			
		||||
@import '~handsontable/dist/handsontable.full.css';
 | 
			
		||||
 | 
			
		||||
@import "~@clr/ui/clr-ui.min.css";
 | 
			
		||||
@import "~@clr/icons/clr-icons.min.css";
 | 
			
		||||
@import '~@clr/icons/clr-icons.min.css';
 | 
			
		||||
 | 
			
		||||
@font-face{
 | 
			
		||||
@import '@cds/core/global.min.css';
 | 
			
		||||
@import '@cds/core/styles/theme.dark.min.css';
 | 
			
		||||
@import '@clr/ui/clr-ui.min.css';
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: text-security-disc;
 | 
			
		||||
  src: url("https://raw.githubusercontent.com/noppa/text-security/master/dist/text-security-disc.woff");
 | 
			
		||||
  src: url('https://raw.githubusercontent.com/noppa/text-security/master/dist/text-security-disc.woff');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body, html {
 | 
			
		||||
  font-weight: 400!important;
 | 
			
		||||
body,
 | 
			
		||||
html {
 | 
			
		||||
  font-weight: 400 !important;
 | 
			
		||||
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
@@ -28,8 +32,16 @@ button {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[cds-text=label] {
 | 
			
		||||
  color: var(--cds-global-typography-color-200);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[cds-text=labelLink] {
 | 
			
		||||
  line-height: 1.8 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Custom loading spinner
 | 
			
		||||
.slider{
 | 
			
		||||
.slider {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  width: 320px;
 | 
			
		||||
  margin-left: 75px;
 | 
			
		||||
@@ -38,33 +50,45 @@ button {
 | 
			
		||||
  overflow-x: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.line{
 | 
			
		||||
  position:absolute;
 | 
			
		||||
.line {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  opacity: 0.4;
 | 
			
		||||
  background:#73D544;
 | 
			
		||||
  width:150%;
 | 
			
		||||
  height:5px;
 | 
			
		||||
  background: #73d544;
 | 
			
		||||
  width: 150%;
 | 
			
		||||
  height: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.subline{
 | 
			
		||||
  position:absolute;
 | 
			
		||||
  background:#73D544;
 | 
			
		||||
  height:5px;
 | 
			
		||||
.subline {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  background: #73d544;
 | 
			
		||||
  height: 5px;
 | 
			
		||||
}
 | 
			
		||||
.inc{
 | 
			
		||||
.inc {
 | 
			
		||||
  animation: increase 2s infinite;
 | 
			
		||||
}
 | 
			
		||||
.dec{
 | 
			
		||||
.dec {
 | 
			
		||||
  animation: decrease 2s 0.5s infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes increase {
 | 
			
		||||
   from { left: -5%; width: 5%; }
 | 
			
		||||
   to { left: 130%; width: 100%;}
 | 
			
		||||
  from {
 | 
			
		||||
    left: -5%;
 | 
			
		||||
    width: 5%;
 | 
			
		||||
  }
 | 
			
		||||
  to {
 | 
			
		||||
    left: 130%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@keyframes decrease {
 | 
			
		||||
   from { left: -80%; width: 80%; }
 | 
			
		||||
   to { left: 110%; width: 10%;}
 | 
			
		||||
  from {
 | 
			
		||||
    left: -80%;
 | 
			
		||||
    width: 80%;
 | 
			
		||||
  }
 | 
			
		||||
  to {
 | 
			
		||||
    left: 110%;
 | 
			
		||||
    width: 10%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
// Custo loading spinner end
 | 
			
		||||
 | 
			
		||||
@@ -248,6 +272,10 @@ button {
 | 
			
		||||
  margin-right: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mr-5i {
 | 
			
		||||
  margin-right: 5px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mr-10 {
 | 
			
		||||
  margin-right: 10px;
 | 
			
		||||
}
 | 
			
		||||
@@ -276,6 +304,10 @@ button {
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mb-10-i {
 | 
			
		||||
  margin-bottom: 10px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mb-20 {
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
@@ -321,11 +353,11 @@ button {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.color-dark-gray {
 | 
			
		||||
  color: #495967
 | 
			
		||||
  color: #495967;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.color-darker-gray{
 | 
			
		||||
  color: #314351
 | 
			
		||||
.color-darker-gray {
 | 
			
		||||
  color: #314351;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.color-white {
 | 
			
		||||
@@ -333,7 +365,7 @@ button {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.color-white-i {
 | 
			
		||||
  color: white !important
 | 
			
		||||
  color: white !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.color-green {
 | 
			
		||||
@@ -341,15 +373,15 @@ button {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.color-dc-green {
 | 
			
		||||
  color: #81b440
 | 
			
		||||
  color: #81b440;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.color-red {
 | 
			
		||||
  color: #e45454
 | 
			
		||||
  color: #e45454;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.color-orange {
 | 
			
		||||
  color: #E67E22;
 | 
			
		||||
  color: #e67e22;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.color-blue {
 | 
			
		||||
@@ -357,7 +389,7 @@ button {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.color-yellow {
 | 
			
		||||
  color: #f1c40f
 | 
			
		||||
  color: #f1c40f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.cursor-pointer {
 | 
			
		||||
@@ -501,7 +533,7 @@ button {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.z-index-highest {
 | 
			
		||||
  z-index: 10000000
 | 
			
		||||
  z-index: 10000000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vertical-align-middle {
 | 
			
		||||
@@ -519,35 +551,36 @@ button {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progresStatic {
 | 
			
		||||
  margin-top:-6px!important;
 | 
			
		||||
  position: absolute!important;
 | 
			
		||||
  z-index: 10000!important;
 | 
			
		||||
  margin-top: -6px !important;
 | 
			
		||||
  position: absolute !important;
 | 
			
		||||
  z-index: 10000 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progress, .progress-static {
 | 
			
		||||
.progress,
 | 
			
		||||
.progress-static {
 | 
			
		||||
  background-color: #f5f6fe;
 | 
			
		||||
  border-radius: 0;
 | 
			
		||||
  font-size: inherit;
 | 
			
		||||
  height: 6px;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  max-height: .583333rem;
 | 
			
		||||
  min-height: .166667rem;
 | 
			
		||||
  max-height: 0.583333rem;
 | 
			
		||||
  min-height: 0.166667rem;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: block;
 | 
			
		||||
  width: calc(100% - 63px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progress.loop:after {
 | 
			
		||||
    -webkit-animation: clr-progress-looper 1.5s ease-in-out infinite;
 | 
			
		||||
    animation: clr-progress-looper 1.5s ease-in-out infinite;
 | 
			
		||||
    content: " ";
 | 
			
		||||
    top: .166667rem;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    display: block;
 | 
			
		||||
    background-color: #60b515;
 | 
			
		||||
    width: 75%;
 | 
			
		||||
  -webkit-animation: clr-progress-looper 1.5s ease-in-out infinite;
 | 
			
		||||
  animation: clr-progress-looper 1.5s ease-in-out infinite;
 | 
			
		||||
  content: ' ';
 | 
			
		||||
  top: 0.166667rem;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  display: block;
 | 
			
		||||
  background-color: #60b515;
 | 
			
		||||
  width: 75%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Fix for clarity bug, should be addressed when clarity is updated
 | 
			
		||||
@@ -570,9 +603,9 @@ button {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.alert-app-level.alert-danger {
 | 
			
		||||
    background: #D94B2E;
 | 
			
		||||
    color: #fff;
 | 
			
		||||
    border: none;
 | 
			
		||||
  background: #d94b2e;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  border: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-header {
 | 
			
		||||
@@ -581,7 +614,7 @@ button {
 | 
			
		||||
 | 
			
		||||
.select select:focus {
 | 
			
		||||
  border-bottom: 1px solid #495967;
 | 
			
		||||
  background: linear-gradient(180deg,transparent 95%,#495a67 0) no-repeat;
 | 
			
		||||
  background: linear-gradient(180deg, transparent 95%, #495a67 0) no-repeat;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clr-treenode-children {
 | 
			
		||||
@@ -597,7 +630,9 @@ button {
 | 
			
		||||
  background: #d8e3e9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
clr-select-container .clr-control-container, clr-select-container .clr-control-container .clr-select-wrapper, clr-select-container select {
 | 
			
		||||
clr-select-container .clr-control-container,
 | 
			
		||||
clr-select-container .clr-control-container .clr-select-wrapper,
 | 
			
		||||
clr-select-container select {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -605,42 +640,55 @@ tbody {
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h3, h4 {
 | 
			
		||||
    color: #585858;
 | 
			
		||||
    font-weight: 400;
 | 
			
		||||
    letter-spacing: normal;
 | 
			
		||||
    line-height: 1rem;
 | 
			
		||||
    margin-top: 1rem;
 | 
			
		||||
    margin-bottom: 0;
 | 
			
		||||
    /* text-transform: uppercase; */
 | 
			
		||||
h3,
 | 
			
		||||
h4 {
 | 
			
		||||
  color: #585858;
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
  letter-spacing: normal;
 | 
			
		||||
  line-height: 1rem;
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
  /* text-transform: uppercase; */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1, h2 {
 | 
			
		||||
    color: #585858;
 | 
			
		||||
    font-weight: 400;
 | 
			
		||||
    /* font-family: Metropolis,Avenir Next,Helvetica Neue,Arial,sans-serif; */
 | 
			
		||||
    letter-spacing: normal;
 | 
			
		||||
    line-height: 2rem;
 | 
			
		||||
    margin-top: 1rem;
 | 
			
		||||
    margin-bottom: 0;
 | 
			
		||||
    /* text-transform: uppercase; */
 | 
			
		||||
h1,
 | 
			
		||||
h2 {
 | 
			
		||||
  color: #585858;
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
  /* font-family: Metropolis,Avenir Next,Helvetica Neue,Arial,sans-serif; */
 | 
			
		||||
  letter-spacing: normal;
 | 
			
		||||
  line-height: 2rem;
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
  /* text-transform: uppercase; */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
clr-icon.is-info {
 | 
			
		||||
    fill: #80b441;
 | 
			
		||||
  fill: #80b441;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.datagrid-host, .datagrid-overlay-wrapper {
 | 
			
		||||
    display: -webkit-box;
 | 
			
		||||
    display: -ms-flexbox;
 | 
			
		||||
    display: -webkit-box!important;
 | 
			
		||||
    -webkit-box-direction: normal;
 | 
			
		||||
.datagrid-host,
 | 
			
		||||
.datagrid-overlay-wrapper {
 | 
			
		||||
  display: -webkit-box;
 | 
			
		||||
  display: -ms-flexbox;
 | 
			
		||||
  display: -webkit-box !important;
 | 
			
		||||
  -webkit-box-direction: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn.btn-danger, .btn.btn-warning {
 | 
			
		||||
    border-color: #ef4f2e;
 | 
			
		||||
    background-color: #D94B2E;
 | 
			
		||||
    color: #fff;
 | 
			
		||||
.btn .clr-loading-btn-content {
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn.btn-danger,
 | 
			
		||||
.btn.btn-warning {
 | 
			
		||||
  border-color: #ef4f2e;
 | 
			
		||||
  background-color: #d94b2e;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Vertical align fix for small buttons with icons
 | 
			
		||||
.btn.btn-sm:has(clr-icon) {
 | 
			
		||||
  line-height: 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.d-none {
 | 
			
		||||
@@ -685,11 +733,16 @@ clr-icon.is-info {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.handsontable td.htInvalid {
 | 
			
		||||
  background: #e62700ad!important;
 | 
			
		||||
    border: 1px solid red !important;
 | 
			
		||||
    color: #ffffff!important;
 | 
			
		||||
  background: #e62700ad !important;
 | 
			
		||||
  border: 1px solid red !important;
 | 
			
		||||
  color: #ffffff !important;
 | 
			
		||||
}
 | 
			
		||||
.margin-top-20{
 | 
			
		||||
 | 
			
		||||
.handsontable .numericListbox {
 | 
			
		||||
  text-align: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.margin-top-20 {
 | 
			
		||||
  margin-top: 20px;
 | 
			
		||||
}
 | 
			
		||||
.hidden {
 | 
			
		||||
@@ -823,7 +876,7 @@ clr-icon.is-info {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.datagrid-body {
 | 
			
		||||
  padding-bottom: 2rem!important;
 | 
			
		||||
  padding-bottom: 2rem !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.abortMsg {
 | 
			
		||||
@@ -831,16 +884,15 @@ clr-icon.is-info {
 | 
			
		||||
  font-family: monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#graph svg {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.no-table-selected {
 | 
			
		||||
  display:flex;
 | 
			
		||||
  justify-content:center;
 | 
			
		||||
  flex-direction:column;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  background: white;
 | 
			
		||||
@@ -851,16 +903,15 @@ clr-icon.is-info {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.copyRight {
 | 
			
		||||
  background:#495967!important;
 | 
			
		||||
  background: #495967 !important;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  display:flex !important;
 | 
			
		||||
  justify-content:center;
 | 
			
		||||
  display: flex !important;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  padding: 5px 0px 4px 0px;
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.nav-tree > clr-tree-node.clr-expanded {
 | 
			
		||||
  display: inline-block !important;
 | 
			
		||||
}
 | 
			
		||||
@@ -903,13 +954,13 @@ clr-tree-node {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tree-search-wrapper {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
 | 
			
		||||
    clr-input-container {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
  clr-input-container {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clr-icon {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
@@ -956,7 +1007,8 @@ input::-ms-clear {
 | 
			
		||||
    overflow: hidden !important;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .clr-treenode-content .clr-icon, .clr-treenode-content clr-icon {
 | 
			
		||||
  .clr-treenode-content .clr-icon,
 | 
			
		||||
  .clr-treenode-content clr-icon {
 | 
			
		||||
    min-width: 16px;
 | 
			
		||||
    min-height: 16px;
 | 
			
		||||
  }
 | 
			
		||||
@@ -985,12 +1037,12 @@ input::-ms-clear {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loadingSpinner {
 | 
			
		||||
  height:70vh;
 | 
			
		||||
  height: 70vh;
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  display:flex;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  flex-direction:column;
 | 
			
		||||
  align-items:center;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.disable-password-manager {
 | 
			
		||||
@@ -1025,7 +1077,8 @@ hr.light {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    min-width: 170px;
 | 
			
		||||
 | 
			
		||||
    clr-icon, .spinner {
 | 
			
		||||
    clr-icon,
 | 
			
		||||
    .spinner {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      right: 19px;
 | 
			
		||||
      top: 0px;
 | 
			
		||||
@@ -1063,7 +1116,7 @@ hr.light {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Firefox */
 | 
			
		||||
  input[type=number] {
 | 
			
		||||
  input[type='number'] {
 | 
			
		||||
    -moz-appearance: textfield;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1076,4 +1129,4 @@ hr.light {
 | 
			
		||||
.link-it {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  text-decoration: underline;
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9226
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9226
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "dcfrontend",
 | 
			
		||||
  "version": "6.2.8",
 | 
			
		||||
  "version": "6.7.0",
 | 
			
		||||
  "description": "Data Controller",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@saithodev/semantic-release-gitea": "^2.1.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,10 @@ _webout = `{"SYSDATE" : "26SEP22"
 | 
			
		||||
"APPROVER": "sasdemo"
 | 
			
		||||
}
 | 
			
		||||
]
 | 
			
		||||
, "histparams":
 | 
			
		||||
[
 | 
			
		||||
{"HIST":100 ,"STARTROW":1 ,"NOBS":3 }
 | 
			
		||||
]
 | 
			
		||||
,"_DEBUG" : ""
 | 
			
		||||
,"_METAUSER": "sasdemo@SAS"
 | 
			
		||||
,"_METAPERSON": "sasdemo"
 | 
			
		||||
 
 | 
			
		||||
@@ -83,6 +83,12 @@ _webout = `{"SYSDATE" : "26SEP22"
 | 
			
		||||
"DC_RESTRICT_EDITRECORD": "NO"
 | 
			
		||||
}
 | 
			
		||||
]
 | 
			
		||||
,"xlmaps":
 | 
			
		||||
[
 | 
			
		||||
["BASEL-CR2" ,"" ,"DC695588.MPE_XLMAP_DATA" ]
 | 
			
		||||
,["BASEL-KM1" ,"Basel 3 Key Metrics report" ,"DC695588.MPE_XLMAP_DATA" ]
 | 
			
		||||
,["SAMPLE" ,"" ,"DC695588.MPE_XLMAP_DATA" ]
 | 
			
		||||
]
 | 
			
		||||
,"_DEBUG" : ""
 | 
			
		||||
,"_METAUSER": "sasdemo@SAS"
 | 
			
		||||
,"_METAPERSON": "sasdemo"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								sas/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								sas/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -7,7 +7,7 @@
 | 
			
		||||
      "name": "dc-sas",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@sasjs/cli": "^4.11.1",
 | 
			
		||||
        "@sasjs/core": "^4.48.4"
 | 
			
		||||
        "@sasjs/core": "^4.52.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@coolaj86/urequest": {
 | 
			
		||||
@@ -116,9 +116,9 @@
 | 
			
		||||
      "integrity": "sha512-Grwydm5GxBsYk238PZw41XPjXVVQ9vWcvfZ06L2P0bQbvK0sGn7l69JA7H5MGr3QcaLpiD4Kg70cAh7PgE+JOw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@sasjs/core": {
 | 
			
		||||
      "version": "4.48.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.48.4.tgz",
 | 
			
		||||
      "integrity": "sha512-KTLHRR47I627NKZG0qMW+wGJP4gFGyeEEyBDsVaSnevdvCz01oP/lpLxx4fIESPQ/YzLNEv+RcP8kcxkdSPQow=="
 | 
			
		||||
      "version": "4.52.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.52.0.tgz",
 | 
			
		||||
      "integrity": "sha512-r+yQYSTYdvNPJ82n2xW/rbJd/cJpk3HuFhCa49RTDlDWjXMXJlymvGB0ltv3/L8rNk4+TwoyMzb7cJYnILi6ag=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@sasjs/lint": {
 | 
			
		||||
      "version": "2.3.1",
 | 
			
		||||
@@ -1828,9 +1828,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@sasjs/core": {
 | 
			
		||||
      "version": "4.48.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.48.4.tgz",
 | 
			
		||||
      "integrity": "sha512-KTLHRR47I627NKZG0qMW+wGJP4gFGyeEEyBDsVaSnevdvCz01oP/lpLxx4fIESPQ/YzLNEv+RcP8kcxkdSPQow=="
 | 
			
		||||
      "version": "4.52.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.52.0.tgz",
 | 
			
		||||
      "integrity": "sha512-r+yQYSTYdvNPJ82n2xW/rbJd/cJpk3HuFhCa49RTDlDWjXMXJlymvGB0ltv3/L8rNk4+TwoyMzb7cJYnILi6ag=="
 | 
			
		||||
    },
 | 
			
		||||
    "@sasjs/lint": {
 | 
			
		||||
      "version": "2.3.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,8 @@
 | 
			
		||||
    "sas9e": "sasjs request services/admin/makedata -d deploy/makeDataSas9.json -t sas9  ",
 | 
			
		||||
    "sas9f": "sasjs request services/admin/refreshtablelineage -t sas9 ",
 | 
			
		||||
    "sas9g": "sasjs request services/admin/refreshcatalog -t sas9",
 | 
			
		||||
    "4gl": "npm run cpfavicon && sasjs cbd -t 4gl && sasjs request services/admin/makedata -d deploy/makeData4GL.json -l sasjsresults/makedata_4gl.log -o sasjsresults/makedata_4gl.json -t 4gl",
 | 
			
		||||
    "4gl": "npm run cpfavicon && sasjs cbd -t 4gl && npm run 4glmakedata",
 | 
			
		||||
    "4glmakedata": "sasjs request services/admin/makedata -d deploy/makeData4GL.json -l sasjsresults/makedata_4gl.log -o sasjsresults/makedata_4gl.json -t 4gl",
 | 
			
		||||
    "server": "npm run cpfavicon && sasjs cbd -t server && npm run serverdata",
 | 
			
		||||
    "server-mihajlo": "npm run cpfavicon && sasjs cbd -t server-mihajlo && npm run serverdata-mihajlo",
 | 
			
		||||
    "serverdata-mihajlo": "sasjs request services/admin/makedata -d deploy/makeDataServer.json -l sasjsresults/makedata_server.log -o sasjsresults/makedata_server.json -t server-mihajlo",
 | 
			
		||||
@@ -28,6 +29,6 @@
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@sasjs/cli": "^4.11.1",
 | 
			
		||||
    "@sasjs/core": "^4.48.4"
 | 
			
		||||
    "@sasjs/core": "^4.52.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								sas/sasjs/db/datactrl/mpe_xlmap_data.ddl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								sas/sasjs/db/datactrl/mpe_xlmap_data.ddl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
/**
 | 
			
		||||
  @file
 | 
			
		||||
  @brief DDL for MPE_XLMAP_DATA
 | 
			
		||||
 | 
			
		||||
  @version 9.3
 | 
			
		||||
  @author 4GL Apps Ltd
 | 
			
		||||
  @copyright 4GL Apps Ltd
 | 
			
		||||
**/
 | 
			
		||||
 | 
			
		||||
create table &curlib..MPE_XLMAP_DATA(
 | 
			
		||||
        LOAD_REF char(32) not null,
 | 
			
		||||
        XLMAP_ID char(32) not null,
 | 
			
		||||
        XLMAP_RANGE_ID char(32) not null,
 | 
			
		||||
        ROW_NO num not null,
 | 
			
		||||
        COL_NO num not null,
 | 
			
		||||
        VALUE_TXT char(4000),
 | 
			
		||||
    constraint pk_MPE_XLMAP_DATA
 | 
			
		||||
      primary key(LOAD_REF, XLMAP_ID, XLMAP_RANGE_ID, ROW_NO, COL_NO));
 | 
			
		||||
							
								
								
									
										17
									
								
								sas/sasjs/db/datactrl/mpe_xlmap_info.ddl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								sas/sasjs/db/datactrl/mpe_xlmap_info.ddl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
/**
 | 
			
		||||
  @file
 | 
			
		||||
  @brief DDL for mpe_xlmap_info
 | 
			
		||||
 | 
			
		||||
  @version 9.3
 | 
			
		||||
  @author 4GL Apps Ltd
 | 
			
		||||
  @copyright 4GL Apps Ltd
 | 
			
		||||
**/
 | 
			
		||||
 | 
			
		||||
create table &curlib..mpe_xlmap_info(
 | 
			
		||||
        tx_from num not null,
 | 
			
		||||
        XLMAP_ID char(32) not null,
 | 
			
		||||
        XLMAP_DESCRIPTION char(1000) not null,
 | 
			
		||||
        XLMAP_TARGETLIBDS char(41) not null,
 | 
			
		||||
        tx_to num not null,
 | 
			
		||||
    constraint pk_mpe_xlmap_info
 | 
			
		||||
        primary key(tx_from,XLMAP_ID));
 | 
			
		||||
							
								
								
									
										19
									
								
								sas/sasjs/db/datactrl/mpe_xlmap_rules.ddl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								sas/sasjs/db/datactrl/mpe_xlmap_rules.ddl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
/**
 | 
			
		||||
  @file
 | 
			
		||||
  @brief DDL for mpe_xlmap_rules
 | 
			
		||||
 | 
			
		||||
  @version 9.3
 | 
			
		||||
  @author 4GL Apps Ltd
 | 
			
		||||
  @copyright 4GL Apps Ltd
 | 
			
		||||
**/
 | 
			
		||||
 | 
			
		||||
create table &curlib..mpe_xlmap_rules(
 | 
			
		||||
        tx_from num not null,
 | 
			
		||||
        XLMAP_ID char(32) not null,
 | 
			
		||||
        XLMAP_RANGE_ID char(32) not null,
 | 
			
		||||
        XLMAP_SHEET char(32) not null,
 | 
			
		||||
        XLMAP_START char(1000) not null,
 | 
			
		||||
        XLMAP_FINISH char(1000),
 | 
			
		||||
        tx_to num not null,
 | 
			
		||||
    constraint pk_mpe_xlmap_rules
 | 
			
		||||
        primary key(tx_from,XLMAP_ID,XLMAP_RANGE_ID));
 | 
			
		||||
							
								
								
									
										83
									
								
								sas/sasjs/db/migrations/20230115_v6.5_release.sas
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								sas/sasjs/db/migrations/20230115_v6.5_release.sas
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
			
		||||
/**
 | 
			
		||||
  @file
 | 
			
		||||
  @brief migration script to move from v5 to v6.5 of data controller
 | 
			
		||||
 | 
			
		||||
**/
 | 
			
		||||
 | 
			
		||||
%let dclib=YOURDCLIB;
 | 
			
		||||
 | 
			
		||||
libname &dclib "/your/dc/path";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
  * Change 1
 | 
			
		||||
  * New MPE_SUBMIT table
 | 
			
		||||
  */
 | 
			
		||||
proc sql;
 | 
			
		||||
create table &dclib..mpe_xlmap_rules(
 | 
			
		||||
        tx_from num not null,
 | 
			
		||||
        XLMAP_ID char(32) not null,
 | 
			
		||||
        XLMAP_RANGE_ID char(32) not null,
 | 
			
		||||
        XLMAP_SHEET char(32) not null,
 | 
			
		||||
        XLMAP_START char(1000) not null,
 | 
			
		||||
        XLMAP_FINISH char(1000),
 | 
			
		||||
        tx_to num not null,
 | 
			
		||||
    constraint pk_mpe_xlmap_rules
 | 
			
		||||
        primary key(tx_from,XLMAP_ID,XLMAP_RANGE_ID));
 | 
			
		||||
 | 
			
		||||
create table &dclib..MPE_XLMAP_DATA(
 | 
			
		||||
        LOAD_REF char(32) not null,
 | 
			
		||||
        XLMAP_ID char(32) not null,
 | 
			
		||||
        XLMAP_RANGE_ID char(32) not null,
 | 
			
		||||
        ROW_NO num not null,
 | 
			
		||||
        COL_NO num not null,
 | 
			
		||||
        VALUE_TXT char(4000),
 | 
			
		||||
    constraint pk_MPE_XLMAP_DATA
 | 
			
		||||
      primary key(LOAD_REF, XLMAP_ID, XLMAP_RANGE_ID, ROW_NO, COL_NO));
 | 
			
		||||
 | 
			
		||||
create table &dclib..mpe_xlmap_info(
 | 
			
		||||
        tx_from num not null,
 | 
			
		||||
        XLMAP_ID char(32) not null,
 | 
			
		||||
        XLMAP_DESCRIPTION char(1000) not null,
 | 
			
		||||
        XLMAP_TARGETLIBDS char(41) not null,
 | 
			
		||||
        tx_to num not null,
 | 
			
		||||
    constraint pk_mpe_xlmap_info
 | 
			
		||||
        primary key(tx_from,XLMAP_ID));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* add mpe_tables entries */
 | 
			
		||||
  insert into &dclib..mpe_tables
 | 
			
		||||
    set tx_from=0
 | 
			
		||||
      ,tx_to='31DEC5999:23:59:59'dt
 | 
			
		||||
      ,libref="&dclib"
 | 
			
		||||
      ,dsn='MPE_XLMAP_INFO'
 | 
			
		||||
      ,num_of_approvals_required=1
 | 
			
		||||
      ,loadtype='TXTEMPORAL'
 | 
			
		||||
      ,var_txfrom='TX_FROM'
 | 
			
		||||
      ,var_txto='TX_TO'
 | 
			
		||||
      ,buskey='XLMAP_ID'
 | 
			
		||||
      ,notes='Docs: https://docs.datacontroller.io/complex-excel-uploads'
 | 
			
		||||
      ,post_edit_hook='services/hooks/mpe_xlmap_info_postedit'
 | 
			
		||||
    ;
 | 
			
		||||
  insert into &dclib..mpe_tables
 | 
			
		||||
    set tx_from=0
 | 
			
		||||
      ,tx_to='31DEC5999:23:59:59'dt
 | 
			
		||||
      ,libref="&dclib"
 | 
			
		||||
      ,dsn='MPE_XLMAP_RULES'
 | 
			
		||||
      ,num_of_approvals_required=1
 | 
			
		||||
      ,loadtype='TXTEMPORAL'
 | 
			
		||||
      ,var_txfrom='TX_FROM'
 | 
			
		||||
      ,var_txto='TX_TO'
 | 
			
		||||
      ,buskey='XLMAP_ID XLMAP_RANGE_ID'
 | 
			
		||||
      ,notes='Docs: https://docs.datacontroller.io/complex-excel-uploads'
 | 
			
		||||
      ,post_edit_hook='services/hooks/mpe_xlmap_rules_postedit'
 | 
			
		||||
    ;
 | 
			
		||||
  insert into &dclib..mpe_tables
 | 
			
		||||
    set tx_from=0
 | 
			
		||||
      ,tx_to='31DEC5999:23:59:59'dt
 | 
			
		||||
      ,libref="&dclib"
 | 
			
		||||
      ,dsn='MPE_XLMAP_DATA'
 | 
			
		||||
      ,num_of_approvals_required=1
 | 
			
		||||
      ,loadtype='UPDATE'
 | 
			
		||||
      ,buskey='LOAD_REF XLMAP_ID XLMAP_RANGE_ID ROW_NO COL_NO'
 | 
			
		||||
      ,notes='Docs: https://docs.datacontroller.io/complex-excel-uploads'
 | 
			
		||||
    ;
 | 
			
		||||
@@ -75,7 +75,9 @@ data basetable;
 | 
			
		||||
run;
 | 
			
		||||
/* create results table */
 | 
			
		||||
data results;
 | 
			
		||||
  format test $7. result $4. reason $50.; stop;
 | 
			
		||||
  format test $7. result $4. reason $50.;
 | 
			
		||||
  call missing(of _all_);
 | 
			
		||||
  stop;
 | 
			
		||||
run;
 | 
			
		||||
 | 
			
		||||
%put assigning lib..;
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user