feat(multi load): refactored range find function, unlocking excel with password is reusable #115

Merged
allan merged 15 commits from issue99 into main 2024-06-27 09:40:45 +00:00
55 changed files with 3113 additions and 1025 deletions

View File

@ -136,7 +136,7 @@ jobs:
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
cat ./cypress.config.ts
# Start frontend and run cypress
npm start & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
npm start & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
- name: Zip Cypress videos
if: always()

View File

@ -15,9 +15,6 @@ context('editor tests: ', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
visitPage('home')
})
@ -118,10 +115,6 @@ context('editor tests: ', function () {
})
})
})
this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
const clickOnEdit = (callback?: any) => {

View File

@ -0,0 +1,224 @@
import { Callbacks } from 'cypress/types/jquery/index'
const username = Cypress.env('username')
const password = Cypress.env('password')
const hostUrl = Cypress.env('hosturl')
const appLocation = Cypress.env('appLocation')
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
const serverType = Cypress.env('serverType')
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
const fixturePath = 'excels_multi_load/'
const library = 'DC996664'
const mpeXTestTable = 'MPE_X_TEST'
const mpeTablesTable = 'MPE_TABLES'
context('excel multi load tests: ', function () {
this.beforeAll(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
cy.loginAndUpdateValidKey(true)
})
mihajlo marked this conversation as resolved
Review

Please clean up

Please clean up
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
visitPage('home/multi-load')
colorLog(
`TEST START ---> ${
Cypress.mocha.getRunner().suite.ctx.currentTest.title
}`,
'#3498DB'
)
})
it('1 | Uploads Excel file with multiple sheets, 3 sheets including data, 2 sheets matched with dataset', (done) => {
attachExcelFile('multi_load_test_2.xlsx', () => {
checkHotUserDatasetTable('hotTableUserDataset', [
[library, mpeXTestTable],
[library, mpeTablesTable]
], () => {
cy.get('#continue-btn').trigger('click').then(() => {
checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], undefined, (includes: boolean) => {
if (includes) {
// MPE_TABLES sheet does not have data so 1 error image must be shown
hasErrorTables(1, (valid: boolean) => {
if (valid) done()
})
}
})
})
})
})
})
it('2 | Uploads Excel file with multiple sheets, 2 sheets matched with dataset, 1 matched sheet does not have data', (done) => {
attachExcelFile('multi_load_test_1.xlsx', () => {
checkHotUserDatasetTable('hotTableUserDataset', [
[library, mpeXTestTable],
[library, mpeTablesTable]
], () => {
cy.get('#continue-btn').trigger('click').then(() => {
checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], `${library}.${mpeXTestTable}`, (includes: boolean) => {
if (includes) {
cy.get('#hotTable').should('be.visible').then(() => {
checkHotUserDatasetTable('hotTable', [
['No', '1', 'more dummy data'],
['No', '1', 'It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:'],
['No', '1', 'if you can fill the unforgiving minute']
], () => {
submitTables()
hasSuccessSubmits(2, (valid: boolean) => {
if (valid) done()
})
})
})
}
})
})
})
})
})
it('3 | Uploads Excel file with multiple sheets, 1 sheets has 2 tables', (done) => {
attachExcelFile('multi_load_test_1.xlsx', () => {
checkHotUserDatasetTable('hotTableUserDataset', [
mihajlo marked this conversation as resolved
Review

variables for these string values would be nice

variables for these string values would be nice
[library, mpeXTestTable],
[library, mpeTablesTable]
], () => {
cy.get('#continue-btn').trigger('click').then(() => {
checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], `${library}.${mpeXTestTable}`, (includes: boolean) => {
if (includes) {
cy.get('#hotTable').should('be.visible').then(() => {
checkHotUserDatasetTable('hotTable', [
['No', '1', 'more dummy data'],
['No', '1', 'It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:'],
['No', '1', 'if you can fill the unforgiving minute']
], () => {
clickOnTreeNode('DC996664.MPE_TABLES', () => {
cy.wait(1000).then(() => {
cy.get('#hotTable').should('be.visible').then(() => {
checkHotUserDatasetTable('hotTable', [
['No', 'DC914286', 'MPE_COLUMN_LEVEL_SECURITY'],
['No', 'DC914286', 'MPE_XLMAP_INFO'],
['No', 'DC914286', 'MPE_XLMAP_RULES']
], () => {
submitTables()
hasSuccessSubmits(2, (valid: boolean) => {
if (valid) done()
})
})
})
})
})
})
})
}
})
})
})
})
})
this.afterEach(() => {
colorLog(`TEST END -------------`, '#3498DB')
mihajlo marked this conversation as resolved
Review

Please clean up

Please clean up
})
})
const attachExcelFile = (excelFilename: string, callback?: any) => {
cy.get('#browse-file')
.should('exist')
.click()
.then(() => {
cy.get('input[type="file"]#file-upload')
.attachFile(`/${fixturePath}/${excelFilename}`)
.then(() => {
if (callback) callback()
})
})
}
const checkHotUserDatasetTable = (hotId: string, dataToContain: any[][], callback?: () => void) => {
cy.get(`#${hotId}`, { timeout: longerCommandTimeout })
.find('div.ht_master.handsontable')
.find('div.wtHolder')
.find('div.wtHider')
.find('div.wtSpreader')
.find('table.htCore')
.find('tbody')
.then((data) => {
cy.wait(2000).then(() => {
for (let rowI = 0; rowI < dataToContain.length; rowI++) {
for (let colI = 0; colI < dataToContain[rowI].length; colI++) {
expect(data[0].children[rowI].children[colI]).to.contain(dataToContain[rowI][colI])
}
}
if (callback) callback()
})
})
}
const clickOnTreeNode = (clickOnNode: string, callback?: () => void) => {
cy.get('.nav-tree clr-tree > clr-tree-node').then((treeNodes: any) => {
for (let node of treeNodes) {
if (node.innerText.toUpperCase().trim().includes(clickOnNode)) {
cy.get(node).trigger('click')
if (callback) callback()
}
}
})
}
const checkIfTreeHasTables = (tables: string[], clickOnNode?: string, callback?: (includes: boolean) => void) => {
cy.get('.nav-tree clr-tree > clr-tree-node').then((treeNodes: any) => {
let datasets = tables
let nodesCorrect = true
let nodeToClick
for (let node of treeNodes) {
if (!datasets.includes(node.innerText.toUpperCase().trim())) {
nodesCorrect = false
}
if (clickOnNode) {
if (node.innerText.toUpperCase().trim().includes(clickOnNode)) {
nodeToClick = node
}
}
}
if (nodeToClick) cy.get(nodeToClick).trigger('click')
if (callback) callback(nodesCorrect)
})
}
const submitTables = () => {
cy.get('#submit-all').trigger('click')
cy.get('#submit-tables').trigger('click')
}
const hasSuccessSubmits = (expectedNoOfSubmits: number, callback: (valid: boolean) => void) => {
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="success"]').should('be.visible').then(($nodes) => {
callback(expectedNoOfSubmits === $nodes.length)
})
}
const hasErrorTables = (expectedNoOfErrors: number, callback: (valid: boolean) => void) => {
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="danger"]').should('be.visible').then(($nodes) => {
callback(expectedNoOfErrors === $nodes.length)
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
const colorLog = (msg: string, color: string) => {
console.log('%c' + msg, 'color:' + color + ';font-weight:bold;')
}

View File

@ -17,9 +17,6 @@ context('excel tests: ', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
visitPage('home')
@ -337,7 +334,6 @@ context('excel tests: ', function () {
this.afterEach(() => {
colorLog(`TEST END -------------`, '#3498DB')
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})

View File

@ -15,9 +15,7 @@ context('filtering tests: ', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout })
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
visitPage('home')
})
@ -174,10 +172,6 @@ context('filtering tests: ', function () {
// })
// })
// })
this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
const checkInfoBarIncludes = (text: string, callback: any) => {

View File

@ -23,15 +23,12 @@ interface EditConfigTableCells {
context('licensing tests: ', function () {
this.beforeAll(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
cy.loginAndUpdateValidKey()
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
visitPage('home')
})
@ -375,9 +372,7 @@ context('licensing tests: ', function () {
})
}
this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
const logout = (callback?: any) => {

View File

@ -18,9 +18,6 @@ context('liveness tests: ', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
visitPage('home')
})

View File

@ -16,7 +16,6 @@ context('editor tests: ', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
cy.wait(2000)
cy.get('body').then(($body) => {
@ -393,10 +392,6 @@ context('editor tests: ', function () {
// }
// )
// })
this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
const removeAllColumns = () => {

View File

@ -15,9 +15,6 @@ context('editor tests: ', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
visitPage('home')
})
@ -118,10 +115,6 @@ context('editor tests: ', function () {
})
})
})
this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
const clickOnEdit = (callback?: any) => {

View File

@ -19,9 +19,6 @@ context('excel tests: ', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
visitPage('home')
@ -339,7 +336,6 @@ context('excel tests: ', function () {
this.afterEach(() => {
colorLog(`TEST END -------------`, '#3498DB')
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})

View File

@ -15,9 +15,6 @@ context('filtering tests: ', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout })
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
visitPage('home')
})
@ -173,10 +170,6 @@ context('filtering tests: ', function () {
})
})
})
this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
const checkInfoBarIncludes = (text: string, callback: any) => {

View File

@ -23,15 +23,11 @@ interface EditConfigTableCells {
context('licensing tests: ', function () {
this.beforeAll(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
cy.loginAndUpdateValidKey()
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
visitPage('home')
})
@ -374,10 +370,6 @@ context('licensing tests: ', function () {
})
})
}
this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
const logout = (callback?: any) => {

View File

@ -18,10 +18,6 @@ context('liveness tests: ', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
visitPage('home')
})

View File

@ -17,7 +17,6 @@ context('editor tests: ', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
cy.wait(2000)
cy.get('body').then(($body) => {
@ -386,10 +385,6 @@ context('editor tests: ', function () {
}
)
})
this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
const checkColumns = (columns: string[], callback: () => void) => {

View File

@ -10,7 +10,7 @@ const check = (cwd) => {
onlyAllow:
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
excludePackages:
'@cds/city@1.1.0;@handsontable/angular@14.3.0;handsontable@14.3.0;hyperformula@2.7.0;jackspeak@2.2.0;path-scurry@1.7.0'
'@cds/city@1.1.0;@handsontable/angular@14.4.0;handsontable@14.4.0;hyperformula@2.7.0;jackspeak@2.2.0;path-scurry@1.7.0'
},
(error, json) => {
if (error) {

105
client/package-lock.json generated
View File

@ -3428,9 +3428,9 @@
"optional": true
},
"node_modules/@cds/core": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@cds/core/-/core-6.11.0.tgz",
"integrity": "sha512-7tNZeLEfpcNdDN85jV9137zxy7euqBCtG8SRNX0E8XGMMODs7JpxvORahhwRGwd7geSbZpA70urNo+h5Y2Mqrg==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@cds/core/-/core-6.12.0.tgz",
"integrity": "sha512-eJpddpt4vx4s0EFi2kAvnRZz1h/JVZPqjkNXpDalBM61IqQp9g2ETGoa594t6BWe3cwGDrkiKuEB+FbAx+IEhw==",
"dependencies": {
"lit": "^2.1.3",
"ramda": "^0.29.0",
@ -4531,9 +4531,9 @@
"dev": true
},
"node_modules/@handsontable/angular": {
"version": "14.3.0",
"resolved": "https://registry.npmjs.org/@handsontable/angular/-/angular-14.3.0.tgz",
"integrity": "sha512-Tdea1fKgQY1DG1upr2I8AlzbYg+kvI4sw4FU/dwWCgKKUkArAT8bW2aOhZMh55CYLJksLFTgCtPSQQMuSeR39g==",
"version": "14.4.0",
"resolved": "https://registry.npmjs.org/@handsontable/angular/-/angular-14.4.0.tgz",
"integrity": "sha512-r984kb/tssJ0nR/uB2bimJWoi+PxoJb4A3RDg1PwAZdquYCPs1j1Yr66N2Z46Ua1Pc1IOnaruLg/kkOuTnPVGg==",
"optionalDependencies": {
"tslib": "^2.3.0"
},
@ -4584,6 +4584,7 @@
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
"deprecated": "Use @eslint/config-array instead",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.2",
@ -4611,6 +4612,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
"integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
"deprecated": "Use @eslint/object-schema instead",
"dev": true
},
"node_modules/@isaacs/cliui": {
@ -6238,9 +6240,9 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
},
"node_modules/@types/lodash": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==",
"version": "4.17.5",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz",
"integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==",
"dev": true
},
"node_modules/@types/lodash-es": {
@ -7071,6 +7073,15 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
"integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
"dev": true,
"peerDependencies": {
"acorn": "^8"
}
},
"node_modules/acorn-import-attributes": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
"integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
"peerDependencies": {
"acorn": "^8"
}
@ -8050,9 +8061,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"version": "4.23.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz",
"integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==",
"funding": [
{
"type": "opencollective",
@ -8068,10 +8079,10 @@
}
],
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
"caniuse-lite": "^1.0.30001629",
"electron-to-chromium": "^1.4.796",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
"update-browserslist-db": "^1.0.16"
},
"bin": {
"browserslist": "cli.js"
@ -8224,9 +8235,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001629",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz",
"integrity": "sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==",
"version": "1.0.30001633",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001633.tgz",
"integrity": "sha512-6sT0yf/z5jqf8tISAgpJDrmwOpLsrpnyCdD/lOZKvKkkJK4Dn0X5i7KF7THEZhOq+30bmhwBlNEaqPUiHiKtZg==",
"funding": [
{
"type": "opencollective",
@ -10137,9 +10148,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.795",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.795.tgz",
"integrity": "sha512-hHo4lK/8wb4NUa+NJYSFyJ0xedNHiR6ylilDtb8NUW9d4dmBFmGiecYEKCEbti1wTNzbKXLfl4hPWEkAFbHYlw=="
"version": "1.4.802",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.802.tgz",
"integrity": "sha512-TnTMUATbgNdPXVSHsxvNVSG0uEd6cSZsANjm8c9HbvflZVVn1yTRcmVXYT1Ma95/ssB/Dcd30AHweH2TE+dNpA=="
},
"node_modules/elliptic": {
"version": "6.5.5",
@ -11509,9 +11520,9 @@
}
},
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.0.tgz",
"integrity": "sha512-CrWQNaEl1/6WeZoarcM9LHupTo3RpZO2Pdk1vktwzPiQTsJnAKJmm3TACKeG5UZbWDfaH2AbvYxzP96y0MT7fA==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
@ -12015,15 +12026,15 @@
}
},
"node_modules/handsontable": {
"version": "14.3.0",
"resolved": "https://registry.npmjs.org/handsontable/-/handsontable-14.3.0.tgz",
"integrity": "sha512-XRKivPK+DkxWTYr+H6ywwLhSn/B5WMj3F3whF7O+Mo6Edm35rF8ydOm6oxVuVWnkvvy4FLqch7wgQk2w6hC8VA==",
"version": "14.4.0",
"resolved": "https://registry.npmjs.org/handsontable/-/handsontable-14.4.0.tgz",
"integrity": "sha512-uTIIx5UgG+2I0flUDj8lxftJAXbBpbkIAFIKugvMW2bNHYNxaNMGAGZ/QFOeCXiYwxeztBATKxMkLpg7N+HrOg==",
"dependencies": {
"@handsontable/pikaday": "^1.0.0",
"@types/pikaday": "1.7.4",
"core-js": "^3.31.1",
"core-js": "^3.37.0",
"dompurify": "^2.1.1",
"moment": "2.29.4",
"moment": "2.30.1",
"numbro": "2.1.2",
"pikaday": "1.8.2"
},
@ -12041,14 +12052,6 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/handsontable/node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
},
"node_modules/has-bigints": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@ -13532,9 +13535,9 @@
}
},
"node_modules/jiti": {
"version": "1.21.3",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.3.tgz",
"integrity": "sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw==",
"version": "1.21.6",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
"dev": true,
"bin": {
"jiti": "bin/jiti.js"
@ -18348,9 +18351,9 @@
}
},
"node_modules/rfdc": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz",
"integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"dev": true
},
"node_modules/rimraf": {
@ -20508,9 +20511,9 @@
}
},
"node_modules/uglify-js": {
"version": "3.17.4",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
"integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
"version": "3.18.0",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.18.0.tgz",
"integrity": "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==",
"dev": true,
"optional": true,
"bin": {
@ -20925,9 +20928,9 @@
}
},
"node_modules/webpack": {
"version": "5.91.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz",
"integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==",
"version": "5.92.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.0.tgz",
"integrity": "sha512-Bsw2X39MYIgxouNATyVpCNVWBCuUwDgWtN78g6lSdPJRLaQ/PUVm/oXcaRAyY/sMFoKFQrsPeqvTizWtq7QPCA==",
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.5",
@ -20935,10 +20938,10 @@
"@webassemblyjs/wasm-edit": "^1.12.1",
"@webassemblyjs/wasm-parser": "^1.12.1",
"acorn": "^8.7.1",
"acorn-import-assertions": "^1.9.0",
"acorn-import-attributes": "^1.9.5",
"browserslist": "^4.21.10",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.16.0",
"enhanced-resolve": "^5.17.0",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",

View File

@ -53,7 +53,6 @@
"@sasjs/utils": "^3.4.0",
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
"@types/d3-graphviz": "^2.6.7",
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
"@types/text-encoding": "0.0.35",
"base64-arraybuffer": "^0.2.0",
"buffer": "^5.4.3",

View File

@ -43,6 +43,10 @@ export interface XLMapListItem {
targetDS: string
}
export interface HandsontableStaticConfig {
darkTableHeaderClass: string
}
/**
* Cached filtering values across whole app (editor, viewer, viewboxes)
* Cached lineage libraries, tables
@ -62,6 +66,7 @@ export const globals: {
viyaApi: any
usernav: any
operators: any
handsontable: HandsontableStaticConfig
[key: string]: any
} = {
rootParam: <string>'',
@ -140,5 +145,8 @@ export const globals: {
operators: {
numOperators: ['=', '<', '>', '<=', '>=', 'BETWEEN', 'IN', 'NOT IN', 'NE'],
charOperators: ['=', '<', '>', '<=', '>=', 'CONTAINS', 'IN', 'NOT IN', 'NE']
},
handsontable: {
darkTableHeaderClass: 'darkTH'
}
}

View File

@ -245,6 +245,7 @@
<app-alerts *ngIf="!errTop"></app-alerts>
<app-requests-modal [(opened)]="requestsModal"></app-requests-modal>
<app-excel-password-modal></app-excel-password-modal>
<!-- <app-terms *ngIf="showRegistration"></app-terms> -->

View File

@ -173,6 +173,12 @@ header {
}
}
.btn-primary .btn, .btn.btn-primary {
&:disabled {
opacity: 0.65;
}
}
.btn {
cursor: pointer;
display: inline-block;

View File

@ -18,10 +18,18 @@ import {
ClarityIcons,
exclamationTriangleIcon,
moonIcon,
sunIcon
sunIcon,
tableIcon,
trashIcon
} from '@cds/core/icon'
ClarityIcons.addIcons(moonIcon, sunIcon, exclamationTriangleIcon)
ClarityIcons.addIcons(
moonIcon,
sunIcon,
exclamationTriangleIcon,
tableIcon,
trashIcon
)
@Component({
selector: 'my-app',

View File

@ -3,7 +3,7 @@
appFileDrop
(fileOver)="fileOverBase($event)"
[uploader]="uploader"
(fileDrop)="getFileDesc($event, true)"
(fileDrop)="attachFile($event, true)"
[clrModalSize]="'xl'"
[clrModalStaticBackdrop]="false"
[clrModalClosable]="excelUploadState === 'Validating-DQ'"
@ -81,7 +81,7 @@
type="file"
appFileSelect
[uploader]="uploader"
(change)="getFileDesc($event)"
(change)="attachFile($event)"
/>
</div>
@ -92,7 +92,7 @@
<button
[disabled]="true"
class="btnView btn btn-sm btn-success profile-buttons w-100"
(click)="getFile()"
(click)="uploadParsedFiles()"
>
Upload
</button>
@ -576,7 +576,7 @@
<button
type="button"
class="btn btn-sm btn-primary"
(click)="getFile(); submitLimitNotice = false"
(click)="uploadParsedFiles(); submitLimitNotice = false"
>
Submit
</button>

View File

@ -201,6 +201,7 @@ hot-table {
display: flex;
justify-content: center;
align-items: flex-start;
margin: 1px;
@ -211,7 +212,10 @@ hot-table {
span {
font-size: 20px;
margin-top: 20px;
color: #fff;
padding: 10px;
background: #dbdbdb;
border-radius: 5px;
color: black;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ 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'
import { MultiDatasetModule } from '../multi-dataset/multi-dataset.module'
const routes: Routes = [
{
@ -11,7 +12,8 @@ const routes: Routes = [
children: [
{ path: '', pathMatch: 'full', redirectTo: 'tables' },
{ path: 'tables', component: HomeComponent },
{ path: 'files', loadChildren: () => XLMapModule }
{ path: 'excel-maps', loadChildren: () => XLMapModule },
{ path: 'multi-load', loadChildren: () => MultiDatasetModule }
]
}
]

View File

@ -0,0 +1 @@
export type FileUploadEncoding = 'UTF-8' | 'WLATIN1'

View File

@ -7,4 +7,5 @@ export default interface SheetInfo {
missingHeaders: string[]
rangeStartRow: number
rangeStartCol: number
rangeAddress?: string
}

View File

@ -0,0 +1,34 @@
import { BaseSASResponse } from './common/BaseSASResponse'
export interface EditorsStageDataSASResponse extends BaseSASResponse {
SYSDATE: string
SYSTIME: string
sasparams: Sasparam[]
_DEBUG: string
_PROGRAM: string
AUTOEXEC: string
MF_GETUSER: string
SYSCC: string
SYSENCODING: string
SYSERRORTEXT: string
SYSHOSTINFOLONG: string
SYSHOSTNAME: string
SYSPROCESSID: string
SYSPROCESSMODE: string
SYSPROCESSNAME: string
SYSJOBID: string
SYSSCPL: string
SYSSITE: string
SYSTCPIPHOSTNAME: string
SYSUSERID: string
SYSVLONG: string
SYSWARNINGTEXT: string
END_DTTM: string
MEMSIZE: string
}
export interface Sasparam {
STATUS: string | 'SUCCESS'
Review

It would be helpful to mention other possible statuses

It would be helpful to mention other possible statuses
Review

There are no more STATUS states, if the upload fails, sasjsAbort will be present in the response

There are no more `STATUS` states, if the upload fails, `sasjsAbort` will be present in the response
DSID: string
URL: string
}

View File

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { MultiDatasetRouteComponent } from '../routes/multi-dataset-route/multi-dataset-route.component'
import { MultiDatasetComponent } from './multi-dataset.component'
const routes: Routes = [
{
path: '',
component: MultiDatasetRouteComponent,
children: [{ path: '', component: MultiDatasetComponent }]
}
]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class MultiDatasetRoutingModule {}

View File

@ -0,0 +1,448 @@
<app-sidebar>
<div *ngIf="datasetsLoading" class="my-10-mx-auto text-center">
<clr-spinner clrMedium></clr-spinner>
</div>
<div *ngIf="!parsedDatasets.length" class="text-center mb-10">
<button
(click)="fileUploadInput.click()"
id="browse-file"
class="btn btn-primary btn-sm"
[disabled]="selectedFile !== null || submittingCsv"
>
Browse file
</button>
<input
hidden
#fileUploadInput
id="file-upload"
type="file"
(change)="onFileChange($event)"
multiple
/>
</div>
<ng-container *ngIf="parsedDatasets.length && !submittedDatasets.length">
<div class="text-center mb-10">
<button (click)="onDiscard()" class="btn btn-danger btn-sm mr-10">
Discard
</button>
<button
(click)="onSubmitAll()"
id="submit-all"
class="btn btn-primary btn-sm"
>
Submit All
</button>
</div>
<p cds-text="caption" class="ml-10 mb-10">Found tables:</p>
<clr-tree>
<clr-tree-node *ngFor="let dataset of parsedDatasets">
<button
(click)="onParsedDatasetClick(dataset)"
class="clr-treenode-link whitespace-nowrap"
[class.active]="dataset.active"
>
<cds-icon
*ngIf="!(dataset.datasource && dataset.parseResult)"
status="danger"
shape="exclamation-circle"
></cds-icon>
<cds-icon
*ngIf="dataset.datasource && dataset.parseResult"
shape="table"
></cds-icon>
{{ dataset.libds }}
</button>
</clr-tree-node>
</clr-tree>
</ng-container>
<ng-container *ngIf="submittedDatasets.length">
<p cds-text="caption" class="ml-10 mb-10 mt-10">Submitted tables:</p>
<clr-tree>
<clr-tree-node *ngFor="let dataset of submittedDatasets">
<button
(click)="onSubmittedDatasetClick(dataset)"
class="clr-treenode-link whitespace-nowrap"
[class.active]="dataset.active"
>
<cds-icon
*ngIf="dataset.error"
status="danger"
shape="exclamation-circle"
></cds-icon>
<cds-icon
*ngIf="dataset.success"
status="success"
shape="check-circle"
></cds-icon>
<cds-icon shape="table"></cds-icon>
{{ dataset.libds }}
</button>
</clr-tree-node>
</clr-tree>
</ng-container>
<!-- <div *ngIf="librariesPaging" class="w-100 text-center">
<span class="spinner spinner-sm"> Loading... </span>
</div> -->
</app-sidebar>
<div class="content-area">
<div class="card no-borders h-100 d-flex clr-flex-column">
<div
class="header-row clr-row justify-content-between clr-justify-content-center w-100 m-0"
>
<p cds-text="section">Multi Dataset Load</p>
</div>
<div
*ngIf="selectedFile === null && !submittingCsv"
class="no-table-selected pointer-events-none"
>
<clr-icon
shape="warning-standard"
size="40"
class="is-info icon-dc-fill"
></clr-icon>
<p class="text-center color-gray mt-10" cds-text="section">
Please upload a file
</p>
</div>
<ng-container *ngIf="selectedFile !== null || submittingCsv">
<ng-container *ngIf="!parsedDatasets.length && selectedFile !== null">
<div class="d-flex clr-justify-content-center mt-15">
<div class="dataset-input-wrapper">
<p cds-text="secondary regular" class="mb-20">
Selected file: <strong>{{ selectedFile.name }}</strong>
<clr-tooltip>
<cds-icon
clrTooltipTrigger
(click)="onDiscardFile()"
shape="trash"
status="danger"
class="ml-5 cursor-pointer"
></cds-icon>
<clr-tooltip-content> Discard the file </clr-tooltip-content>
</clr-tooltip>
</p>
<p cds-text="secondary regular" class="mb-15">
Paste or type the list of datasets to upload:
</p>
<clr-control-helper class="mb-5"
>Each row is one dataset. We will automatically detect tables by
the sheetname and populate if any.</clr-control-helper
>
<hot-table
hotId="hotInstanceUserDataset"
id="hotTableUserDataset"
class="mt-15"
[afterGetColHeader]="afterGetColHeader"
[settings]="hotUserDatasets"
[licenseKey]="hotTableLicenseKey"
stretchH="all"
>
</hot-table>
<div class="dataset-selection-actions text-right mt-10">
<button
(click)="onUploadFile()"
id="continue-btn"
class="btn btn-primary btn-sm"
[disabled]="!matchedDatasets.length"
[clrLoading]="uploadLoading"
>
Continue
</button>
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="parsedDatasets.length && !submittedDatasets.length">
<div
*ngIf="!activeParsedDataset"
class="no-table-selected pointer-events-none"
>
<clr-icon
shape="warning-standard"
size="40"
class="is-info icon-dc-fill"
></clr-icon>
<p class="text-center color-gray mt-10" cds-text="section">
Please select a dataset on the left to review the data
</p>
</div>
<ng-container *ngIf="activeParsedDataset">
<div class="d-flex clr-justify-content-between p-10">
<div>
<p cds-text="secondary regular" class="mb-10">
Found in range:
<ng-container *ngIf="activeParsedDataset.parseResult">
<strong
>"{{
activeParsedDataset.parseResult.rangeSheetRes?.sheetName
}}"!{{
activeParsedDataset.parseResult.rangeSheetRes
?.rangeAddress
}}</strong
>
</ng-container>
<ng-container *ngIf="!activeParsedDataset.parseResult">
<strong>No data found</strong>
</ng-container>
</p>
<p cds-text="secondary regular">
Dataset:
<strong>
<clr-tooltip>
<a
clrTooltipTrigger
[routerLink]="'/editor/' + activeParsedDataset.libds"
>{{ activeParsedDataset.libds }}</a
>
<clr-tooltip-content
[clrPosition]="'top-right'"
[clrSize]="'sm'"
>
Click to edit the table
</clr-tooltip-content>
</clr-tooltip>
</strong>
</p>
</div>
<div>
<clr-toggle-wrapper>
<input
type="checkbox"
clrToggle
[(ngModel)]="activeParsedDataset.includeInSubmission"
name="options"
[disabled]="
!(
activeParsedDataset.datasource &&
activeParsedDataset.parseResult
)
"
required
value="option1"
/>
<label>Include in submission</label>
</clr-toggle-wrapper>
</div>
</div>
<div *ngIf="isHotHidden" class="text-center w-100">
<clr-spinner class="spinner-md"></clr-spinner>
</div>
<hot-table
hotId="hotInstance"
id="hotTable"
class="mt-15"
[afterGetColHeader]="afterGetColHeader"
[className]="['htDark', 'htCustomHidden']"
[licenseKey]="hotTableLicenseKey"
[multiColumnSorting]="true"
[viewportRowRenderingOffset]="50"
[manualColumnResize]="true"
[filters]="true"
stretchH="all"
>
</hot-table>
</ng-container>
</ng-container>
<ng-container *ngIf="submittedDatasets.length">
<div
*ngIf="!activeSubmittedDataset"
class="no-table-selected pointer-events-none"
>
<clr-icon
shape="warning-standard"
size="40"
class="is-info icon-dc-fill"
></clr-icon>
<p class="text-center color-gray mt-10" cds-text="section">
Please select a dataset on the left to review the submit results
</p>
</div>
</ng-container>
<ng-container *ngIf="activeSubmittedDataset">
<div class="d-flex clr-justify-content-between p-10">
<div>
<p
*ngIf="activeSubmittedDataset.parseResult"
cds-text="secondary regular"
class="mb-10"
>
Found in range:
<strong
>"{{
activeSubmittedDataset.parseResult.rangeSheetRes?.sheetName
}}"!{{
activeSubmittedDataset.parseResult.rangeSheetRes?.rangeAddress
}}</strong
>
</p>
<p cds-text="secondary regular" class="mb-10">
Matched with dataset:
<strong>
<clr-tooltip>
<a
clrTooltipTrigger
[routerLink]="'/editor/' + activeSubmittedDataset.libds"
>{{ activeSubmittedDataset.libds }}</a
>
<clr-tooltip-content
[clrPosition]="'top-right'"
[clrSize]="'sm'"
>
Click to edit the table
</clr-tooltip-content>
</clr-tooltip>
</strong>
</p>
<p cds-text="secondary regular" class="mb-10">
Status:
<span *ngIf="activeSubmittedDataset.success" class="color-green"
><strong>SUCCESS</strong></span
>
<span *ngIf="activeSubmittedDataset.error" class="color-red"
><strong>ERROR</strong></span
>
</p>
<p
*ngIf="activeSubmittedDataset.error"
cds-text="secondary regular"
>
Error details:
</p>
</div>
<div>
<button
*ngIf="!submittingCsv && activeSubmittedDataset.error"
(click)="reSubmitTable(activeSubmittedDataset)"
class="btn btn-primary mt-10"
[clrLoading]="submitLoading"
>
Resubmit
</button>
<button
(click)="
downloadFile(
activeSubmittedDataset.success || activeSubmittedDataset.error
)
"
class="btn btn-primary-outline mt-10"
>
Download log
</button>
</div>
</div>
<div *ngIf="activeSubmittedDataset.error" class="error-field mt-15">
<div class="log-wrapper">
{{ activeSubmittedDataset.error | json }}
</div>
</div>
</ng-container>
</ng-container>
<!-- <div>
<p
*ngIf="
licenceState.value.viewer_rows_allowed !== Infinity &&
hotTable.data &&
hotTable.data.length > licenceState.value.viewer_rows_allowed
"
class="mt-2-i w-100 text-center"
>
To display more than {{ licenceState.value.viewer_rows_allowed }} rows,
contact <contact-link />
</p>
</div> -->
</div>
</div>
<clr-modal [(clrModalOpen)]="showSubmitReasonModal" [clrModalClosable]="false">
<h3 class="modal-title">
Submit {{ tablesToSubmit.length }}
{{ tablesToSubmit.length === 1 ? 'table' : 'tables' }} for approval
</h3>
<div class="modal-body">
<p
*ngIf="licenceState.value.submit_rows_limit !== Infinity"
cds-text="body"
class="licence-limit-notice mt-0 mb-15"
>
Due to current licence, only
{{ licenceState.value.submit_rows_limit }} rows in each file will be
submitted. To remove the restriction, contact
support&#64;datacontroller.io.
</p>
<div class="text-area-full-width">
<label for="formFields_8" class="mb-5 d-block">Message</label>
<textarea
clrTextarea
[(ngModel)]="submitReasonMessage"
tabindex="0"
class="submit-reason"
type="text"
id="formFields_8"
></textarea>
</div>
<p cds-text="caption_clean" class="mt-10">
Tables will be sent sequentially, logs will be available after all tables
are submitted.
</p>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-outline"
[disabled]="submitLoading"
(click)="showSubmitReasonModal = false"
>
Cancel
</button>
<button
type="button"
id="submit-tables"
class="btn btn-primary"
[clrLoading]="submitLoading"
(click)="submitTables()"
>
Submit
</button>
</div>
</clr-modal>
<clr-modal [(clrModalOpen)]="csvSubmitting" [clrModalClosable]="false">
<h3 class="modal-title">
Submitting {{ csvFiles.length }} CSV
{{ csvFiles.length === 1 ? 'file' : 'files' }}
</h3>
<div class="modal-body">
<div class="text-center">
<clr-spinner clrMedium></clr-spinner>
</div>
<p cds-text="caption_clean" class="mt-10 text-center">
This will take few moments
</p>
</div>
</clr-modal>

View File

@ -0,0 +1,50 @@
.no-table-selected {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
position: absolute;
background: var(--clr-vertical-nav-bg-color);
z-index: 10;
width: 100%;
height: 100%;
top: 0;
}
.header-row {
padding: 15px 0;
border-bottom: 1px solid #d3d3d3;
}
.dataset-input-wrapper {
max-width: 500px;
width: 100%;
textarea {
min-height: 200px;
height: 200px;
}
}
.submit-reason {
min-height: 70px;
max-height: 70px;
height: 70px;
}
.log-wrapper {
margin: 0 10px;
height: auto;
}
::ng-deep td.not-matched {
background-color: #ff000054;
}
.dataset-selection-actions {
border-top: 1px solid #d3d3d3;
}
.licence-limit-notice {
color: var(--cds-alias-status-warning-dark);
}

File diff suppressed because it is too large Load Diff

View 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 { DcTreeModule } from '../shared/dc-tree/dc-tree.module'
import { MultiDatasetComponent } from './multi-dataset.component'
import { MultiDatasetRoutingModule } from './multi-dataset-routing.module'
import { MultiDatasetRouteComponent } from '../routes/multi-dataset-route/multi-dataset-route.component'
// register Handsontable's modules
registerAllModules()
@NgModule({
declarations: [MultiDatasetRouteComponent, MultiDatasetComponent],
imports: [
HotTableModule,
MultiDatasetRoutingModule,
FormsModule,
ClarityModule,
AppSharedModule,
CommonModule,
DcTreeModule,
DirectivesModule
],
exports: [MultiDatasetComponent]
})
export class MultiDatasetModule {}

View File

@ -0,0 +1 @@
<router-outlet></router-outlet>

View File

@ -0,0 +1,16 @@
import { Component, OnInit, OnDestroy } from '@angular/core'
@Component({
selector: 'app-multi-dataset-route',
templateUrl: './multi-dataset-route.component.html',
host: {
class: 'content-container'
}
})
export class MultiDatasetRouteComponent implements OnInit, OnDestroy {
constructor() {}
ngOnInit() {}
ngOnDestroy() {}
}

View File

@ -17,10 +17,10 @@ import { LoggerService } from './logger.service'
import { isSpecialMissing } from '@sasjs/utils/input/validators'
import { Col } from '../shared/dc-validator/models/col.model'
import { get } from 'lodash-es'
import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model'
@Injectable()
export class SasStoreService {
public libds!: string
public response: Subject<any> = new Subject<any>()
public changedTable: Subject<any> = new Subject<any>()
public details: Subject<any> = new Subject<any>()
@ -56,7 +56,6 @@ export class SasStoreService {
program: string,
libds: string
) {
this.libds = libds
const tables: any = {}
tables[tableName] = [tableData]
const res: EditorsGetDataSASResponse = await this.sasService.request(
@ -65,7 +64,7 @@ export class SasStoreService {
)
const response: EditorsGetDataServiceResponse = {
data: res,
libds: this.libds
libds: libds
}
return response
}
@ -84,8 +83,9 @@ export class SasStoreService {
tableData: any,
tableName: string,
program: string,
$dataFormats: $DataFormats | null
) {
$dataFormats: $DataFormats | null,
suppressErrorSuccessMessages?: boolean
): Promise<EditorsStageDataSASResponse> {
// add sp as third argument of createData call
let tables: any = {
@ -100,7 +100,15 @@ export class SasStoreService {
tables[tableName] = [tableParams]
let res: any = await this.sasService.request(program, tables)
let res = await this.sasService.request<EditorsStageDataSASResponse>(
mihajlo marked this conversation as resolved Outdated
Outdated
Review

it would be nice to define response type

it would be nice to define response type
program,
tables,
null,
{
suppressErrorAbortModal: suppressErrorSuccessMessages,
suppressSuccessAbortModal: suppressErrorSuccessMessages
}
)
return res
}

View File

@ -93,15 +93,16 @@ export class SasService {
* @param url service to run reuqest against
* @param data to be sent to backend service
* @param config additional parameters to force eg. { debug: false }
* @param wrapperOptions used to suppress error or success abort modals after request is finished
* @param wrapperOptions used to provide options to the request wrapper function
* for example to suppress error or success abort modals after request is finished
* @returns
*/
public request(
public request<responseType = any>(
url: string,
data: any,
config?: any,
wrapperOptions?: RequestWrapperOptions
): Promise<any> {
): Promise<responseType> {
url = 'services/' + url
if (!wrapperOptions) wrapperOptions = {}

View File

@ -0,0 +1,906 @@
import { Injectable } from '@angular/core'
import * as XLSX from '@sheet/crypto'
import {
ExcelPasswordModalService,
Result
} from '../shared/excel-password-modal/excel-password-modal.service'
import { EventService } from './event.service'
import { isSpecialMissing } from '@sasjs/utils/input/validators'
import {
dateFormat,
dateToUtcTime,
dateToTime
} from '../editor/utils/date.utils'
import {
excelDateToJSDate,
getMissingHeaders
} from '../editor/utils/grid.utils'
import { isStringNumber, isStringDecimal } from '../editor/utils/types.utils'
import SheetInfo from '../models/SheetInfo'
import { blobToFile } from '../xlmap/utils/file.utils'
import { ExcelRule } from '../models/TableData'
import { DcValidator } from '../shared/dc-validator/dc-validator'
import { LicenceService } from './licence.service'
import { FileUploadEncoding } from '../models/FileUploadEncoding'
import { FileUploader } from '../models/FileUploader.class'
/**
* Used in combination with buffer
*/
const iconv = require('iconv-lite')
/**
* In combination with `iconv` is used for encoding json data captured with sheet js from excel file into a file again
* Which will be send to backend
*/
const Buffer = require('buffer/').Buffer
type AOA = any[][]
export interface ParseParams {
file: File
dcValidator: DcValidator
/**
* Parse function will manipulate and return the uploader array which can be provided with files already in the queue
* Otherwise new empty instance will be created.
*/
uploader?: FileUploader
headerPks: string[]
headerArray: string[]
headerShow: string[]
timeHeaders: string[]
dateHeaders: string[]
dateTimeHeaders: string[]
xlRules: ExcelRule[]
encoding?: FileUploadEncoding
}
export interface ParseResult {
/**
* In case of CSV file, won't be returned
yury marked this conversation as resolved
Review

one-line comment would make more sense

one-line comment would make more sense
Review

If it's one line it's not detected as a comment by the IntelliSense.

image

If it's one line it's not detected as a comment by the `IntelliSense`. ![image](/attachments/ff7dece9-c909-48bf-965b-f379ab58ba17)
*/
data?: any[]
/**
* In case of CSV file, won't be returned
Review

one-line comment would make more sense

one-line comment would make more sense
Review

If it's one line it's not detected as a comment by the IntelliSense.

If it's one line it's not detected as a comment by the IntelliSense.
*/
headerShow?: string[]
rangeSheetRes?: SheetInfo
uploader: FileUploader
}
@Injectable({
providedIn: 'root'
})
export class SpreadsheetService {
private licenceState = this.licenceService.licenceState
constructor(
private excelPasswordModalService: ExcelPasswordModalService,
private eventService: EventService,
private licenceService: LicenceService
) {}
/**
* Parses attached file and searches fo the matching data
*
* @param parseParams params required for parsing the file
* @param onParseStateChange callback used to inform about parsing state
* so the user of the function can update the UI with latest info
* @param onTableFoundEvent callback fired when table range is found in the file
*
* @returns parsed list of files to upload and JSON data ready for HOT usage
*/
public parseExcelFile(
parseParams: ParseParams,
onParseStateChange?: (uploadState: string) => void,
onTableFoundEvent?: (info: string) => void
): Promise<ParseResult | undefined> {
return new Promise((resolve, reject) => {
let data: any[] = []
const uploader: FileUploader = parseParams.uploader || new FileUploader()
mihajlo marked this conversation as resolved Outdated
Outdated
Review

should be const

should be const
const file: File = parseParams.file
mihajlo marked this conversation as resolved Outdated
Outdated
Review

should be const

should be const
const filename = file.name
Outdated
Review

should be const

should be const
if (!parseParams.encoding) parseParams.encoding = 'UTF-8'
if (onParseStateChange)
onParseStateChange(`Loading ${filename} into the browser`)
let foundData = {
sheet: ''
}
let fileType = filename.slice(
filename.lastIndexOf('.') + 1,
filename.lastIndexOf('.') + 4
)
if (fileType.toLowerCase() === 'xls') {
let reader: FileReader = new FileReader()
const self = this
reader.onload = async (theFile: any) => {
/* read workbook */
const bstr = this.toBstr(theFile.target.result)
let wb: XLSX.WorkBook | undefined = undefined
let fileUnlocking: boolean = false
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) {
if (err.message.toLowerCase().includes('password')) {
fileUnlocking = true
while (fileUnlocking) {
const password = await this.promptExcelPassword()
if (password) {
try {
wb = XLSX.read(bstr, {
...xlsxOptions,
password: password
})
fileUnlocking = false
} catch (err: any) {
this.excelPasswordModalService.open({
error: true
})
if (!err.message.toLowerCase().includes('password')) {
fileUnlocking = false
}
}
} else {
fileUnlocking = false
}
}
} else {
return reject('Error reading the file')
}
}
if (!wb) {
return reject('No workbook found.')
}
/* save data */
let isComplete: boolean = false
let missingHeaders: string[] = []
const csvArrayHeaders: string[] = [
'_____DELETE__THIS__RECORD_____',
...parseParams.headerArray
]
let csvArrayHeadersLower = csvArrayHeaders.map((x) => x.toLowerCase())
let csvArrayHeadersMap = csvArrayHeadersLower.reduce(
(map: any, obj: string) => {
map[obj] = -1
return map
},
{}
)
let csvArrayData: any[] = []
const rangeSheetRes: SheetInfo = this.getRangeAndSheet(
wb,
parseParams
)
missingHeaders = rangeSheetRes.missingHeaders
if (rangeSheetRes.foundData) {
isComplete = true
csvArrayHeadersMap = rangeSheetRes.csvArrayHeadersMap
const ws: XLSX.WorkSheet = wb.Sheets[rangeSheetRes.sheetName]
if (onParseStateChange)
onParseStateChange(
`Table found on sheet ${rangeSheetRes.sheetName} on row ${rangeSheetRes.startRow}`
)
let startAddress = ''
let endAddress = ''
for (
let row = rangeSheetRes.startRow;
row < rangeSheetRes.endRow;
++row
) {
const arr: any[] = []
csvArrayHeadersLower.forEach((x) => {
const col = csvArrayHeadersMap[x]
const addr = XLSX.utils.encode_cell({
r: rangeSheetRes.rangeStartRow + row,
c: rangeSheetRes.rangeStartCol + col
})
let cell
if (!ws[addr]) {
cell = { v: '' }
} else {
cell = ws[addr]
}
if (startAddress === '' && ws[addr]) startAddress = addr
endAddress = addr
arr.push(cell)
})
// If we found at least one non empty value it means it is not empty row
// othervise, it is empty row
let arrNonEmptyValue = arr.find((x) => x.v !== '')
if (arrNonEmptyValue) csvArrayData.push(arr)
}
rangeSheetRes.rangeAddress = `${startAddress}:${endAddress}`
if (onTableFoundEvent)
onTableFoundEvent(
`Sheet: ${rangeSheetRes.sheetName}\nRange: ${rangeSheetRes.rangeAddress}`
)
} else {
missingHeaders = rangeSheetRes.missingHeaders
}
if (missingHeaders.length > 0) {
missingHeaders.sort(function compareSecondColumn(a, b) {
if (a[1] === b[1]) {
return 0
} else {
return a[1] > b[1] ? -1 : 1
}
})
let abortMsg = missingHeaders
.map((x) => x[0])
.slice(0, 5)
.join('\n')
uploader.queue.pop()
return reject(abortMsg)
}
// If first row is empty, that means no data has been found
if (csvArrayData.length === 0 || csvArrayData[0].length === 0) {
let abortMsg = 'No relevant data found in File !'
uploader.queue.pop()
return reject(abortMsg)
}
if (
parseParams.dateTimeHeaders.length > 0 ||
parseParams.dateHeaders.length > 0 ||
parseParams.timeHeaders.length > 0
) {
csvArrayData = this.updateDateTimeCols(
csvArrayHeaders,
csvArrayData,
parseParams
)
}
if (parseParams.xlRules.length > 0) {
csvArrayData = this.updateXLRuleCols(
csvArrayHeaders,
csvArrayData,
parseParams
)
}
if (!isComplete) {
let abortMsg = ''
if (missingHeaders.length === 0) {
abortMsg = 'No relevant data found in File !'
} else {
missingHeaders.sort(function compareSecondColumn(a, b) {
if (a[1] === b[1]) {
return 0
} else {
return a[1] > b[1] ? -1 : 1
}
})
abortMsg = missingHeaders
.map((x) => x[0])
.slice(0, 5)
.join('\n')
}
// abort message is fired, return undefined
uploader.queue.pop()
return reject(abortMsg)
} else {
parseParams.headerShow = csvArrayHeaders
csvArrayData = csvArrayData.map((row: any) =>
row.map((col: any) => (col.t === 'n' ? col.v : col.w))
)
csvArrayData = csvArrayData.map((row: any) => {
return row.map((col: any, index: number) => {
if (!col && col !== 0) col = ''
/**
* Keeping this for the reference
* Code below used to convert JSON to CSV
* now the XLSX is converting to CSV
*/
// if (isNaN(col)) {
mihajlo marked this conversation as resolved
Review

Please clean up

Please clean up
// // 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.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 = parseParams.headerShow[index]
const colRule = parseParams.dcValidator?.getRule(colName)
if (colRule?.type === 'numeric') {
if (isSpecialMissing(col) && !col.includes('.'))
col = '.' + col
}
return col
})
})
data = csvArrayData
// 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
// Blob from which CSV file will be created depending of the selected
// encoding
let blob: Blob
if (parseParams.encoding === 'WLATIN1') {
// WLATIN1
let encoded = iconv.decode(
Buffer.from(csvContentClean),
'CP-1252'
)
blob = new Blob([encoded], { type: 'application/csv' })
} else {
// UTF-8
blob = new Blob([csvContentClean], { type: 'application/csv' })
}
let newCSVFile: File = blobToFile(blob, filename + '.csv')
uploader.addToQueue([newCSVFile])
}
if (data.length === 0) {
return reject(
`Table in the file is empty. Data found on sheet: ${foundData.sheet}`
)
}
return resolve({
uploader,
data,
rangeSheetRes,
headerShow: parseParams.headerShow
})
}
reader.readAsArrayBuffer(file)
} else if (fileType.toLowerCase() === 'csv') {
if (this.licenceState.value.submit_rows_limit !== Infinity) {
uploader.queue.pop()
return reject(
'Excel files only. To unlock CSV uploads, please contact support@datacontroller.io'
)
}
if (parseParams.encoding === 'WLATIN1') {
let reader = new FileReader()
const self = this
// Closure to capture the file information.
reader.onload = (theFile: any) => {
let encoded = iconv.decode(
Buffer.from(theFile.target.result),
'CP-1252'
)
let blob = new Blob([encoded], { type: fileType })
let encodedFile: File = blobToFile(blob, filename)
uploader.queue.pop()
uploader.addToQueue([encodedFile])
resolve({
uploader
})
}
reader.readAsArrayBuffer(file)
} else {
return resolve({
uploader
})
}
} else {
let abortMsg =
'Invalid file type "<b>' +
filename +
'</b>". Please upload csv or excel file.'
uploader.queue.pop()
return reject(abortMsg)
}
})
}
/**
* Function that gives the sheet name which contains data and range of data in that sheet, if some headers are missing then also gives the info about those missing headers
* @param wb Excel workbook
* @returns {object: SheetInfo} an object which contains necessary information about workbook that which sheet contains required data and what's the range
*/
private getRangeAndSheet(
wb: XLSX.WorkBook,
parseParams: ParseParams
): SheetInfo {
let data = []
let rangeStartRow: number = 0
let rangeStartCol: number = 0
let startRow: number = -1
let endRow: number = -1
let sheetName: string = ''
let isComplete = false
let missingHeaders: string[] = []
const csvArrayHeaders: string[] = [
'_____DELETE__THIS__RECORD_____',
mihajlo marked this conversation as resolved
Review

should be a const

should be a const
...parseParams.headerArray
]
let csvArrayHeadersLower = csvArrayHeaders.map((x) => x.toLowerCase())
let csvArrayHeadersMap = csvArrayHeadersLower.reduce(
(map: any, obj: string) => {
map[obj] = -1
return map
},
{}
)
wb.SheetNames.forEach((element: string) => {
// Checking for required data in each sheet in workbook/
if (isComplete) {
return
}
missingHeaders = []
sheetName = element
const ws: XLSX.WorkSheet = wb.Sheets[sheetName]
data = <AOA>XLSX.utils.sheet_to_json(ws, {
header: 1,
blankrows: true, // Without empty rows, if another table is below a table separated by the empty rows, startRow index is wrong
defval: ''
})
if (data.length <= 1) {
return
}
let tempArr: string[] = []
parseParams.headerArray.forEach(() => tempArr.push(''))
data.push(tempArr)
let foundHeaders = false
data.forEach((row: any, index: number) => {
if (isComplete) {
return
}
if (foundHeaders) {
let isDataEnd = true
let isPkNull = false
csvArrayHeadersLower.forEach((x) => {
const col = csvArrayHeadersMap[x]
if (row[col] !== '' && row[col] !== undefined) {
isDataEnd = false
} else {
if (parseParams.headerPks.indexOf(x.toUpperCase()) !== -1) {
isPkNull = true
}
}
})
if (isDataEnd || isPkNull) {
endRow = index
isComplete = true
} else {
if (startRow === -1) {
startRow = index
}
}
} else {
const rowLowerCase: string[] = row.map((x: any) =>
x.toString().toLowerCase()
)
// If in file there is no delete column, remove it from search of missing.
// This way delete column will be optional to provide in file
if (!rowLowerCase.includes('_____delete__this__record_____')) {
const deleteIndex = csvArrayHeadersLower.indexOf(
'_____delete__this__record_____'
)
if (deleteIndex > -1) csvArrayHeadersLower.splice(deleteIndex, 1)
}
foundHeaders = true
csvArrayHeadersLower.forEach((x) => {
if (rowLowerCase.indexOf(x) === -1) {
foundHeaders = false
}
})
let result = []
result = this.findValidHeaders(
rowLowerCase,
csvArrayHeadersLower,
index,
sheetName,
parseParams
)
if (result[0] === false) {
foundHeaders = false
if (result[1].length > 0) {
result[1].forEach((data: string) => {
missingHeaders.push(data)
})
}
} else {
csvArrayHeadersMap = result[1]
}
}
})
if (isComplete) {
this.update_sheet_range(ws)
const worksheetSel = ws['!ref']
if (worksheetSel) {
const range = XLSX.utils.decode_range(ws['!ref'] || '')
rangeStartRow = range.s.r
rangeStartCol = range.s.c
}
}
})
// If start row is still -1 that means first row of found range is empty
if (startRow === -1) isComplete = false
const returnObj: SheetInfo = {
foundData: isComplete,
sheetName,
startRow,
endRow,
csvArrayHeadersMap,
missingHeaders,
rangeStartRow,
rangeStartCol
}
return returnObj
}
private findValidHeaders(
row: string[],
headers: string[],
rowNumber: number,
tabName: string,
parseParams: ParseParams
): Array<any> {
let headersFound = false
const missingErrorArray = []
mihajlo marked this conversation as resolved Outdated
Outdated
Review

should be const

should be const
let j = 0
while (j < row.length) {
if (headersFound) {
// return;
} else {
if (headers.indexOf(row[j]) !== -1) {
let breakIndex
let rowStart = 0
let rowEnd = 0
let arrStart = 0
let foundHeadersArray: string[] = []
let spaceBreak = false
for (let i = j; i < row.length; i++) {
if (
row[i] === '' ||
(foundHeadersArray.indexOf(row[i]) !== -1 &&
this.isColHeader(row[i], parseParams.headerArray))
) {
if (row[i] === '') {
spaceBreak = true
}
breakIndex = i
break
} else {
foundHeadersArray.push(row[i])
}
}
let tempArray: string[] = []
if (breakIndex !== undefined) {
tempArray = row.slice(j, breakIndex)
arrStart = j
rowEnd = breakIndex
if (spaceBreak) {
rowStart = j
j = breakIndex
} else {
rowStart = j
j = breakIndex - 1
}
} else {
tempArray = row.slice(j)
rowStart = j
arrStart = j
rowEnd = row.length
j = row.length
}
let foundHeaders = true
//We check if there are missing headers
headers.forEach((x) => {
if (tempArray.indexOf(x) === -1) {
foundHeaders = false
}
})
if (foundHeaders) {
headersFound = true
let mapHeaders: any[] = headers
let csvArrayHeadersMap = mapHeaders.reduce(function (map, obj) {
map[obj] = -1
return map
}, {})
let temp = row.slice(rowStart, rowEnd)
headers.forEach((x) => {
csvArrayHeadersMap[x] = temp.indexOf(x) + rowStart
})
return [true, csvArrayHeadersMap]
} else {
let missingHeaders = getMissingHeaders(tempArray, headers)
let missingMessage = '<b>TAB(' + tabName + ')</b>'
missingErrorArray.push([
missingMessage +
' - ' +
missingHeaders[1].join(',') +
' ( missing ' +
missingHeaders[0].join(',') +
' )',
missingHeaders[1].length
])
}
}
}
j++
}
return [false, missingErrorArray]
}
private isColHeader(col: string, headerArray: string[]) {
return headerArray.indexOf(col.toUpperCase()) > -1
}
/**
* Function that updates the !ref range value provided in official docs.
* @param ws worksheet to be updated
*/
private update_sheet_range(ws: XLSX.WorkSheet) {
const range = { s: { r: Infinity, c: Infinity }, e: { r: 0, c: 0 } }
Outdated
Review

lint check should throw a warning

`lint` check should throw a warning

Apparently prettier does not care about that stuff, only the format.

Apparently prettier does not care about that stuff, only the format.
Object.keys(ws)
.filter(function (x) {
return x.charAt(0) != '!'
})
.map(XLSX.utils.decode_cell)
.forEach(function (x: any) {
range.s.c = Math.min(range.s.c, x.c)
range.s.r = Math.min(range.s.r, x.r)
range.e.c = Math.max(range.e.c, x.c)
range.e.r = Math.max(range.e.r, x.r)
})
ws['!ref'] = XLSX.utils.encode_range(range)
}
/**
* When excel is password protected we will display the password prompt for user to type password in.
* @returns Password user input or undefined if discarded by user
*/
private promptExcelPassword(): Promise<string | undefined> {
return new Promise((resolve, reject) => {
this.excelPasswordModalService.open().subscribe((result: Result) => {
resolve(result.password)
})
})
mihajlo marked this conversation as resolved
Review

please clean up

please clean up
}
private updateDateTimeCols(
headers: any,
data: any,
parseParams: ParseParams
) {
if (parseParams.dateHeaders.length > 0) {
const dateCols: number[] = []
parseParams.dateHeaders.forEach((element) => {
if (headers.indexOf(element) !== -1) {
dateCols.push(headers.indexOf(element))
}
})
data.forEach((row: any[]) => {
dateCols.forEach((element) => {
const obj = row[element]
if (isStringNumber(obj.v)) {
const date = excelDateToJSDate(Number(obj.v))
obj.v =
date.getFullYear() +
'-' +
('0' + (date.getMonth() + 1)).slice(-2) +
'-' +
('0' + date.getDate()).slice(-2)
} else {
if (obj && obj.v && obj.v.toString().indexOf(':') === -1) {
mihajlo marked this conversation as resolved
Review

should be const

should be const
const date = new Date(obj.v)
if (date.toUTCString() !== 'Invalid Date') {
obj.v = dateFormat(date)
}
}
}
row[element] = obj
})
})
}
if (parseParams.timeHeaders.length > 0) {
let timeCols: number[] = []
parseParams.timeHeaders.forEach((element) => {
if (headers.indexOf(element) !== -1) {
timeCols.push(headers.indexOf(element))
}
})
data.forEach((row: any[]) => {
timeCols.forEach((element) => {
const obj = row[element]
if (
isStringNumber(obj.v) ||
isStringDecimal(obj.v) ||
obj.v.includes('E-')
) {
const date = excelDateToJSDate(Number(obj.v))
obj.v = dateToUtcTime(date)
}
row[element] = obj
})
})
}
if (parseParams.dateTimeHeaders.length > 0) {
let dateTimeCols: number[] = []
parseParams.dateTimeHeaders.forEach((element) => {
if (headers.indexOf(element) !== -1) {
dateTimeCols.push(headers.indexOf(element))
}
})
data.forEach((row: any[]) => {
dateTimeCols.forEach((element) => {
const obj = row[element]
if (isStringNumber(obj.v) || isStringDecimal(obj.v)) {
const date = excelDateToJSDate(Number(obj.v))
obj.v = dateFormat(date) + ' ' + dateToUtcTime(date)
} else {
if (obj.v.indexOf(' ') === -1 && obj.v.indexOf(':') !== -1) {
let str = obj.v.substring(0, obj.v.indexOf(':'))
str = str + ' ' + obj.v.substring(obj.v.indexOf(':') + 1)
obj.v = str
}
const date = new Date(obj.v)
if (date.toUTCString() !== 'Invalid Date') {
Review

should be const

should be const
obj.v = dateFormat(date) + ' ' + dateToTime(date)
}
}
row[element] = obj
})
})
}
return data
}
mihajlo marked this conversation as resolved
Review

should be const

should be const
private updateXLRuleCols(headers: any, data: any, parseParams: ParseParams) {
if (parseParams.xlRules.length > 0) {
const xlRuleCols: any = []
parseParams.xlRules.forEach((element: any) => {
if (headers.indexOf(element.XL_COLUMN) !== -1) {
element['index'] = headers.indexOf(element.XL_COLUMN)
xlRuleCols.push(element)
}
mihajlo marked this conversation as resolved
Review

should be const

should be const
})
data.forEach((row: any[]) => {
xlRuleCols.forEach((element: any) => {
const obj = row[element.index]
if (element.XL_RULE === 'FORMULA') {
if ('f' in obj) {
if (obj['t'] === 'n') {
obj['v'] = '=' + obj['f']
} else {
obj['w'] = '=' + obj['f']
}
}
}
row[element] = obj
})
})
}
return data
}
private toBstr(res: any) {
let bytes = new Uint8Array(res)
let binary = ''
let length = bytes.byteLength
for (let i = 0; i < length; i++) {
binary += String.fromCharCode(bytes[i])
}
return binary
}
}

View File

@ -0,0 +1,38 @@
<ng-container *ngIf="options$ | async as options">
<clr-modal
[clrModalOpen]="options.open"
[clrModalSize]="'md'"
[clrModalClosable]="false"
>
<h3 class="modal-title center text-center color-darker-gray">
Password Protected File
</h3>
<div class="modal-body d-flex clr-justify-content-center">
<p class="m-0">Please enter password:</p>
<input
#filePasswordInput
data-lpignore="true"
autocomplete="off"
id="filePasswordInput"
type="text"
class="clr-input disable-password-manager"
/>
</div>
<div class="modal-footer">
<p *ngIf="options.error" class="m-0 color-red">
Sorry that didn't work, try again.
</p>
<button type="button" class="btn btn-sm btn-outline" (click)="close()">
Cancel
</button>
<button
type="button"
class="btn btn-sm btn-success-outline"
[disabled]="filePasswordInput.value.length < 1"
(click)="close(filePasswordInput.value)"
>
Unlock
</button>
</div>
</clr-modal>
</ng-container>

View File

@ -0,0 +1,24 @@
import { Component } from '@angular/core'
import { Observable } from 'rxjs'
import {
ExcelPasswordModalService,
Options
} from './excel-password-modal.service'
@Component({
selector: 'app-excel-password-modal',
templateUrl: './excel-password-modal.component.html'
})
export class ExcelPasswordModalComponent {
options$: Observable<Options>
fileUnlockError: boolean = false
constructor(private excelPasswordModalService: ExcelPasswordModalService) {
this.options$ = this.excelPasswordModalService.optionsSubject$
}
close(password?: string) {
this.excelPasswordModalService.close(password)
}
}

View File

@ -0,0 +1,45 @@
import { Injectable } from '@angular/core'
import { Subject, Observable } from 'rxjs'
export interface Options extends OpenOptions {
open: boolean
}
export interface Result {
password: string | undefined
}
export interface OpenOptions {
error?: boolean
}
@Injectable({
providedIn: 'root'
})
export class ExcelPasswordModalService {
public optionsSubject$: Subject<Options> = new Subject()
public resultChange$: Subject<Result> = new Subject()
constructor() {}
public open(openOptions?: OpenOptions): Observable<Result> {
this.optionsSubject$.next({
open: true,
...openOptions
})
this.resultChange$ = new Subject<Result>()
return this.resultChange$.asObservable()
}
close(password?: string) {
this.optionsSubject$.next({
open: false
})
this.resultChange$.next({
password
})
this.resultChange$.complete()
}
}

View File

@ -61,20 +61,6 @@
}
}
.log-wrapper {
min-height: 50px;
padding: 10px;
margin-top: 10px;
white-space: pre-wrap;
border-radius: 3px;
border: 1px solid #e2e2e2;
height: 48vh;
overflow: auto;
}
.no-reqs {
border-top: 1px solid #0000001a;
padding-top: 5px;

View File

@ -14,6 +14,7 @@ import { TermsComponent } from './terms/terms.component'
import { DirectivesModule } from '../directives/directives.module'
import { DatasetInfoComponent } from './dataset-info/dataset-info.component'
import { ContactLinkComponent } from './contact-link/contact-link.component'
import { ExcelPasswordModalComponent } from './excel-password-modal/excel-password-modal.component'
@NgModule({
imports: [
@ -30,7 +31,8 @@ import { ContactLinkComponent } from './contact-link/contact-link.component'
AlertsComponent,
TermsComponent,
DatasetInfoComponent,
ContactLinkComponent
ContactLinkComponent,
ExcelPasswordModalComponent
],
exports: [
LoadingIndicatorComponent,
@ -39,7 +41,8 @@ import { ContactLinkComponent } from './contact-link/contact-link.component'
AlertsComponent,
TermsComponent,
DatasetInfoComponent,
ContactLinkComponent
ContactLinkComponent,
ExcelPasswordModalComponent
],
providers: [UserService, AlertsService]
})

View File

@ -124,8 +124,17 @@
routerLinkActive="active"
>Tables</a
>
<a clrVerticalNavLink routerLink="/home/files" routerLinkActive="active"
>Files</a
<a
clrVerticalNavLink
routerLink="/home/excel-maps"
routerLinkActive="active"
>Excel Maps</a
>
<a
clrVerticalNavLink
routerLink="/home/multi-load"
routerLinkActive="active"
>Multi Load</a
>
</clr-dropdown-menu>
</clr-dropdown>

View File

@ -0,0 +1,13 @@
import { globals } from '../../_globals'
/**
* Function reused in HOT instances to add a class used for dark mode
*/
export const baseAfterGetColHeader = (
column: number,
TH: HTMLTableCellElement,
headerLevel: number
) => {
mihajlo marked this conversation as resolved Outdated
Outdated
Review

darkTH should be a const

`darkTH` should be a const
// Dark mode
TH.classList.add(globals.handsontable.darkTableHeaderClass)
}

View File

@ -434,7 +434,7 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// Dark mode
th.classList.add('darkTH')
th.classList.add(globals.handsontable.darkTableHeaderClass)
}
})
hotInstance?.render()

View File

@ -35,7 +35,7 @@ export class StageComponent implements OnInit {
maxRows: this.licenceState.value.stage_rows_allowed || Infinity,
afterGetColHeader: (column, th, headerLevel) => {
// Dark mode
th.classList.add('darkTH')
th.classList.add(globals.handsontable.darkTableHeaderClass)
}
}
@ -66,7 +66,7 @@ export class StageComponent implements OnInit {
)
if (xlmap) {
const id = this.hotTable.data[0].XLMAP_ID
this.route.navigateByUrl('/home/files/' + id)
this.route.navigateByUrl('/home/excel-maps/' + id)
} else {
this.route.navigateByUrl('/editor/' + this.tableDetails.BASE_TABLE)
}

View File

@ -9,7 +9,7 @@ import {
import { SasStoreService } from '../services/sas-store.service'
import { Subscription } from 'rxjs'
import { Params, Router } from '@angular/router'
import { Router } from '@angular/router'
import { ActivatedRoute } from '@angular/router'
import { globals } from '../_globals'
@ -18,7 +18,6 @@ import { HelperService } from '../services/helper.service'
import { HotTableRegisterer } from '@handsontable/angular'
import { SasService } from '../services/sas.service'
import { SASjsConfig } from '@sasjs/adapter'
import { AppService } from '../services/app.service'
import { QueryComponent } from '../query/query.component'
import { FilterGroup, FilterQuery } from '../models/FilterQuery'
@ -118,7 +117,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
},
afterGetColHeader: (col: number, th: any, headerLevel: number) => {
// Dark mode
th.classList.add('darkTH')
th.classList.add(globals.handsontable.darkTableHeaderClass)
},
rowHeaderWidth: 15,
rowHeights: 20,
@ -1076,7 +1075,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// Dark mode
th.classList.add('darkTH')
th.classList.add(globals.handsontable.darkTableHeaderClass)
}
})
}

View File

@ -39,8 +39,6 @@ clr-tree-node button {
}
.content-area {
padding: 0.5rem !important;
display: flex;
flex-direction: column;
}

View File

@ -145,7 +145,7 @@ export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
public afterGetColHeader(column: number, th: any) {
// Dark mode
th.classList.add('darkTH')
th.classList.add(globals.handsontable.darkTableHeaderClass)
}
public xlmapOnClick(xlmap: XLMapListItem) {
@ -159,7 +159,7 @@ export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
}
this.selectedTab = Tabs.Rules
this.viewXLMapRules()
this.router.navigateByUrl('/home/files/' + xlmap.id)
this.router.navigateByUrl('/home/excel-maps/' + xlmap.id)
}
}

View File

@ -1,4 +1,6 @@
$headerBackground: #314351;
$trackColor: #3b5268;
$thumbColor: #273849;
$thumbColor: #273849;
$codeBorder: #e2e2e2;

View File

@ -84,6 +84,31 @@ body[cds-theme="light"] {
line-height: 1.8 !important;
}
[cds-text=caption_clean] {
font-size: var(--cds-global-typography-caption-font-size);
font-weight: var(--cds-global-typography-caption-font-weight);
line-height: var(--cds-global-typography-caption-line-height);
letter-spacing: var(--cds-global-typography-caption-letter-spacing);
&::after, &::before {
display: none;
}
}
.log-wrapper {
min-height: 50px;
padding: 10px;
margin-top: 10px;
white-space: pre-wrap;
border-radius: 3px;
border: 1px solid $codeBorder;
mihajlo marked this conversation as resolved Outdated
Outdated
Review

colour should be a const

colour should be a const
height: 48vh;
overflow: auto;
}
// Custom loading spinner
.slider {
position: absolute;
@ -200,6 +225,10 @@ body[cds-theme="light"] {
width: 100%;
}
.w-100-i {
width: 100% !important;
}
.w-40 {
width: 40%;
}
@ -288,6 +317,10 @@ body[cds-theme="light"] {
margin-left: 5px;
}
.ml-5-i {
margin-left: 5px !important;
}
.ml-10 {
margin-left: 10px;
}
@ -458,6 +491,10 @@ body[cds-theme="light"] {
pointer-events: none;
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-center {
text-align: center;
}
@ -784,6 +821,10 @@ clr-icon.is-info {
box-shadow: none !important;
}
.htCustomHidden {
display: none;
}
body[cds-theme="dark"] {
.htDark {
background: #888;
@ -1040,7 +1081,6 @@ clr-tree-node {
padding: 0px 8px 0px 8px;
width: auto;
height: auto;
display: flex;
align-items: center;
}