feat: export test run result (#220)
This commit is contained in:
@@ -106,11 +106,13 @@ app.use('/attachments', attachmentsDeleteRoute);
|
|||||||
app.use('/attachments', attachmentsDownloadRoute);
|
app.use('/attachments', attachmentsDownloadRoute);
|
||||||
|
|
||||||
// "/runs"
|
// "/runs"
|
||||||
|
const runsDownloadRoute = require('./routes/runs/download')(sequelize);
|
||||||
const runsIndexRoute = require('./routes/runs/index')(sequelize);
|
const runsIndexRoute = require('./routes/runs/index')(sequelize);
|
||||||
const runsShowRoute = require('./routes/runs/show')(sequelize);
|
const runsShowRoute = require('./routes/runs/show')(sequelize);
|
||||||
const runsNewRoute = require('./routes/runs/new')(sequelize);
|
const runsNewRoute = require('./routes/runs/new')(sequelize);
|
||||||
const runsEditRoute = require('./routes/runs/edit')(sequelize);
|
const runsEditRoute = require('./routes/runs/edit')(sequelize);
|
||||||
const runDeleteRoute = require('./routes/runs/delete')(sequelize);
|
const runDeleteRoute = require('./routes/runs/delete')(sequelize);
|
||||||
|
app.use('/runs', runsDownloadRoute);
|
||||||
app.use('/runs', runsIndexRoute);
|
app.use('/runs', runsIndexRoute);
|
||||||
app.use('/runs', runsShowRoute);
|
app.use('/runs', runsShowRoute);
|
||||||
app.use('/runs', runsNewRoute);
|
app.use('/runs', runsNewRoute);
|
||||||
|
|||||||
170
backend/package-lock.json
generated
170
backend/package-lock.json
generated
@@ -18,7 +18,8 @@
|
|||||||
"papaparse": "^5.5.2",
|
"papaparse": "^5.5.2",
|
||||||
"sequelize": "^6.37.5",
|
"sequelize": "^6.37.5",
|
||||||
"sequelize-cli": "^6.6.2",
|
"sequelize-cli": "^6.6.2",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7",
|
||||||
|
"xmlbuilder2": "^3.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@gar/promisify": {
|
"node_modules/@gar/promisify": {
|
||||||
@@ -219,6 +220,50 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
||||||
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
|
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@oozcitak/dom": {
|
||||||
|
"version": "1.15.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
|
||||||
|
"integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/infra": "1.0.8",
|
||||||
|
"@oozcitak/url": "1.0.4",
|
||||||
|
"@oozcitak/util": "8.3.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/infra": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/util": "8.3.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/url": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/infra": "1.0.8",
|
||||||
|
"@oozcitak/util": "8.3.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/util": {
|
||||||
|
"version": "8.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
|
||||||
|
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@@ -396,6 +441,19 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "1.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
|
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||||
|
"dependencies": {
|
||||||
|
"sprintf-js": "~1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/argparse/node_modules/sprintf-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
|
||||||
|
},
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
@@ -1087,6 +1145,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
||||||
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
|
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/esprima": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||||
|
"bin": {
|
||||||
|
"esparse": "bin/esparse.js",
|
||||||
|
"esvalidate": "bin/esvalidate.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/etag": {
|
"node_modules/etag": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
@@ -1772,6 +1842,18 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-yaml": {
|
||||||
|
"version": "3.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||||
|
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^1.0.7",
|
||||||
|
"esprima": "^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsbn": {
|
"node_modules/jsbn": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||||
@@ -3436,6 +3518,20 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/xmlbuilder2": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/dom": "1.15.10",
|
||||||
|
"@oozcitak/infra": "1.0.8",
|
||||||
|
"@oozcitak/util": "8.3.8",
|
||||||
|
"js-yaml": "3.14.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
@@ -3637,6 +3733,38 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
||||||
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
|
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
|
||||||
},
|
},
|
||||||
|
"@oozcitak/dom": {
|
||||||
|
"version": "1.15.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
|
||||||
|
"integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==",
|
||||||
|
"requires": {
|
||||||
|
"@oozcitak/infra": "1.0.8",
|
||||||
|
"@oozcitak/url": "1.0.4",
|
||||||
|
"@oozcitak/util": "8.3.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@oozcitak/infra": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==",
|
||||||
|
"requires": {
|
||||||
|
"@oozcitak/util": "8.3.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@oozcitak/url": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==",
|
||||||
|
"requires": {
|
||||||
|
"@oozcitak/infra": "1.0.8",
|
||||||
|
"@oozcitak/util": "8.3.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@oozcitak/util": {
|
||||||
|
"version": "8.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
|
||||||
|
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="
|
||||||
|
},
|
||||||
"@pkgjs/parseargs": {
|
"@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@@ -3777,6 +3905,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"argparse": {
|
||||||
|
"version": "1.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
|
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||||
|
"requires": {
|
||||||
|
"sprintf-js": "~1.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"sprintf-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"array-flatten": {
|
"array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
@@ -4323,6 +4466,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"esprima": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||||
|
},
|
||||||
"etag": {
|
"etag": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
@@ -4826,6 +4974,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"js-yaml": {
|
||||||
|
"version": "3.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||||
|
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||||
|
"requires": {
|
||||||
|
"argparse": "^1.0.7",
|
||||||
|
"esprima": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"jsbn": {
|
"jsbn": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||||
@@ -6049,6 +6206,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
},
|
},
|
||||||
|
"xmlbuilder2": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==",
|
||||||
|
"requires": {
|
||||||
|
"@oozcitak/dom": "1.15.10",
|
||||||
|
"@oozcitak/infra": "1.0.8",
|
||||||
|
"@oozcitak/util": "8.3.8",
|
||||||
|
"js-yaml": "3.14.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"xtend": {
|
"xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"papaparse": "^5.5.2",
|
"papaparse": "^5.5.2",
|
||||||
"sequelize": "^6.37.5",
|
"sequelize": "^6.37.5",
|
||||||
"sequelize-cli": "^6.6.2",
|
"sequelize-cli": "^6.6.2",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7",
|
||||||
|
"xmlbuilder2": "^3.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,14 +30,20 @@ module.exports = function (sequelize) {
|
|||||||
return res.status(404).send('No cases found');
|
return res.status(404).send('No cases found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const csv = Papa.unparse(cases, {
|
if (type === 'json') {
|
||||||
quotes: true,
|
return res.json(cases);
|
||||||
skipEmptyLines: true,
|
} else if (type === 'csv') {
|
||||||
});
|
const csv = Papa.unparse(cases, {
|
||||||
|
quotes: true,
|
||||||
|
skipEmptyLines: true,
|
||||||
|
});
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||||
res.setHeader('Content-Disposition', `attachment; filename=cases_folder_${folderId}.csv`);
|
res.setHeader('Content-Disposition', `attachment; filename=cases_folder_${folderId}.csv`);
|
||||||
res.send(csv);
|
return res.send(csv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).json({ error: 'Unsupported type. Use ?type=json or ?type=csv' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.status(500).send('Internal Server Error');
|
res.status(500).send('Internal Server Error');
|
||||||
|
|||||||
126
backend/routes/runs/download.js
Normal file
126
backend/routes/runs/download.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const Papa = require('papaparse');
|
||||||
|
const { create } = require('xmlbuilder2');
|
||||||
|
|
||||||
|
const defineRun = require('../../models/runs');
|
||||||
|
const defineRunCase = require('../../models/runCases');
|
||||||
|
const defineCase = require('../../models/cases');
|
||||||
|
const defineFolder = require('../../models/folders');
|
||||||
|
|
||||||
|
module.exports = function (sequelize) {
|
||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
|
||||||
|
const { verifyProjectVisibleFromRunId } = require('../../middleware/verifyVisible')(sequelize);
|
||||||
|
|
||||||
|
const Run = defineRun(sequelize, DataTypes);
|
||||||
|
const RunCase = defineRunCase(sequelize, DataTypes);
|
||||||
|
const Case = defineCase(sequelize, DataTypes);
|
||||||
|
const Folder = defineFolder(sequelize, DataTypes);
|
||||||
|
|
||||||
|
RunCase.belongsTo(Case, { foreignKey: 'caseId' });
|
||||||
|
|
||||||
|
router.get('/download/:runId', verifySignedIn, verifyProjectVisibleFromRunId, async (req, res) => {
|
||||||
|
const { runId } = req.params;
|
||||||
|
const { type } = req.query;
|
||||||
|
|
||||||
|
if (!runId) {
|
||||||
|
return res.status(400).json({ error: 'runId is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const run = await Run.findByPk(runId);
|
||||||
|
if (!run) {
|
||||||
|
return res.status(404).send('Run not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const runCases = await RunCase.findAll({
|
||||||
|
where: { runId },
|
||||||
|
include: [{ model: Case }],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (type === 'xml') {
|
||||||
|
// JUnit xml valid status
|
||||||
|
const validStatuses = [1, 2, 4]; // 0: untested, 1: passed, 2 failed, 3: retest, 4: skipped
|
||||||
|
const filteredRunCases = runCases.filter((rc) => validStatuses.includes(rc.status));
|
||||||
|
|
||||||
|
// group cases by folder
|
||||||
|
const folderMap = new Map();
|
||||||
|
for (const rc of filteredRunCases) {
|
||||||
|
const folderId = rc.Case.folderId;
|
||||||
|
if (!folderMap.has(folderId)) {
|
||||||
|
folderMap.set(folderId, []);
|
||||||
|
}
|
||||||
|
folderMap.get(folderId).push(rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct JUnit xml
|
||||||
|
const xml = create({ version: '1.0' });
|
||||||
|
const root = xml.ele('testsuites');
|
||||||
|
|
||||||
|
for (const [folderId, cases] of folderMap.entries()) {
|
||||||
|
let folderName = '';
|
||||||
|
const folder = await Folder.findByPk(folderId);
|
||||||
|
if (folder) {
|
||||||
|
folderName = folder.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const suite = root.ele('testsuite', {
|
||||||
|
name: folderName,
|
||||||
|
tests: cases.length,
|
||||||
|
failures: cases.filter((c) => c.status === 2).length,
|
||||||
|
skipped: cases.filter((c) => c.status === 4).length,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const rc of cases) {
|
||||||
|
const testCase = suite.ele('testcase', {
|
||||||
|
name: rc.Case.title,
|
||||||
|
classname: folderName,
|
||||||
|
time: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rc.status === 2) {
|
||||||
|
testCase.ele('failure', { message: 'Test failed' }).txt('Test case failed.');
|
||||||
|
} else if (rc.status === 4) {
|
||||||
|
testCase.ele('skipped', { message: 'skipped' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const xmlString = xml.end({ prettyPrint: true });
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/xml');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename=run_${runId}.xml`);
|
||||||
|
return res.send(xmlString);
|
||||||
|
} else if (type === 'json') {
|
||||||
|
return res.json(runCases);
|
||||||
|
} else if (type === 'csv') {
|
||||||
|
const records = runCases.map((rc) => ({
|
||||||
|
id: rc.Case.id,
|
||||||
|
title: rc.Case.title,
|
||||||
|
state: rc.Case.state,
|
||||||
|
priority: rc.Case.priority,
|
||||||
|
type: rc.Case.type,
|
||||||
|
automationStatus: rc.Case.automationStatus,
|
||||||
|
status: rc.status,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const csv = Papa.unparse(records, {
|
||||||
|
quotes: true,
|
||||||
|
skipEmptyLines: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename=run_${runId}.csv`);
|
||||||
|
return res.send(csv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).json({ error: 'Unsupported type. Use ?type=xml or ?type=json or ?type=csv' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
"close": "Close",
|
"close": "Close",
|
||||||
"are_you_sure": "Are you sure you want to delete test cases?",
|
"are_you_sure": "Are you sure you want to delete test cases?",
|
||||||
"new_test_case": "New Test Case",
|
"new_test_case": "New Test Case",
|
||||||
"download_csv": "Download CSV",
|
"export": "Export",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"no_cases_found": "No test cases found",
|
"no_cases_found": "No test cases found",
|
||||||
"case_title": "Test Case Title",
|
"case_title": "Test Case Title",
|
||||||
@@ -254,6 +254,7 @@
|
|||||||
"updating": "Updating...",
|
"updating": "Updating...",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"updated_test_run": "Updated test run",
|
"updated_test_run": "Updated test run",
|
||||||
|
"export": "Export",
|
||||||
"progress": "Progress",
|
"progress": "Progress",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
|||||||
@@ -189,7 +189,7 @@
|
|||||||
"close": "閉じる",
|
"close": "閉じる",
|
||||||
"are_you_sure": "テストケースを削除してもよろしいですか?",
|
"are_you_sure": "テストケースを削除してもよろしいですか?",
|
||||||
"new_test_case": "新規テストケース",
|
"new_test_case": "新規テストケース",
|
||||||
"download_csv": "CSVダウンロード",
|
"export": "エクスポート",
|
||||||
"status": "ステータス",
|
"status": "ステータス",
|
||||||
"no_cases_found": "テストケースがありません",
|
"no_cases_found": "テストケースがありません",
|
||||||
"case_title": "テストケースタイトル",
|
"case_title": "テストケースタイトル",
|
||||||
@@ -255,6 +255,7 @@
|
|||||||
"updating": "更新中...",
|
"updating": "更新中...",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"updated_test_run": "テストランを更新しました",
|
"updated_test_run": "テストランを更新しました",
|
||||||
|
"export": "エクスポート",
|
||||||
"progress": "進捗",
|
"progress": "進捗",
|
||||||
"refresh": "再読み込み",
|
"refresh": "再読み込み",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useState, useEffect, useContext } from 'react';
|
|||||||
import TestCaseTable from './TestCaseTable';
|
import TestCaseTable from './TestCaseTable';
|
||||||
import CaseDialog from './CaseDialog';
|
import CaseDialog from './CaseDialog';
|
||||||
import { TokenContext } from '@/utils/TokenProvider';
|
import { TokenContext } from '@/utils/TokenProvider';
|
||||||
import { fetchCases, createCase, deleteCases, csvDownload } from '@/utils/caseControl';
|
import { fetchCases, createCase, deleteCases, exportCases } from '@/utils/caseControl';
|
||||||
import { CaseType, CasesMessages } from '@/types/case';
|
import { CaseType, CasesMessages } from '@/types/case';
|
||||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
||||||
import { PriorityMessages } from '@/types/priority';
|
import { PriorityMessages } from '@/types/priority';
|
||||||
@@ -74,8 +74,8 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCsvDownload = async () => {
|
const onExportCases = async (type: string) => {
|
||||||
await csvDownload(context.token.access_token, Number(folderId));
|
await exportCases(context.token.access_token, Number(folderId), type);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -87,7 +87,7 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
|||||||
onCreateCase={() => setIsCaseDialogOpen(true)}
|
onCreateCase={() => setIsCaseDialogOpen(true)}
|
||||||
onDeleteCase={onDeleteCase}
|
onDeleteCase={onDeleteCase}
|
||||||
onDeleteCases={onDeleteCases}
|
onDeleteCases={onDeleteCases}
|
||||||
onCsvDownload={onCsvDownload}
|
onExportCases={onExportCases}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
priorityMessages={priorityMessages}
|
priorityMessages={priorityMessages}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import {
|
|||||||
DropdownItem,
|
DropdownItem,
|
||||||
Selection,
|
Selection,
|
||||||
SortDescriptor,
|
SortDescriptor,
|
||||||
|
ButtonGroup,
|
||||||
} from '@heroui/react';
|
} from '@heroui/react';
|
||||||
import { Plus, MoreVertical, Trash, Download } from 'lucide-react';
|
import { Plus, MoreVertical, Trash, Download, FileDown, ChevronDown, FileJson, FileSpreadsheet } from 'lucide-react';
|
||||||
import { Link } from '@/src/i18n/routing';
|
import { Link } from '@/src/i18n/routing';
|
||||||
import { CaseType, CasesMessages } from '@/types/case';
|
import { CaseType, CasesMessages } from '@/types/case';
|
||||||
import { PriorityMessages } from '@/types/priority';
|
import { PriorityMessages } from '@/types/priority';
|
||||||
@@ -28,7 +29,7 @@ type Props = {
|
|||||||
onCreateCase: () => void;
|
onCreateCase: () => void;
|
||||||
onDeleteCase: (caseId: number) => void;
|
onDeleteCase: (caseId: number) => void;
|
||||||
onDeleteCases: (caseIds: number[]) => void;
|
onDeleteCases: (caseIds: number[]) => void;
|
||||||
onCsvDownload: () => void;
|
onExportCases: (type: string) => void;
|
||||||
messages: CasesMessages;
|
messages: CasesMessages;
|
||||||
priorityMessages: PriorityMessages;
|
priorityMessages: PriorityMessages;
|
||||||
locale: LocaleCodeType;
|
locale: LocaleCodeType;
|
||||||
@@ -41,7 +42,7 @@ export default function TestCaseTable({
|
|||||||
onCreateCase,
|
onCreateCase,
|
||||||
onDeleteCase,
|
onDeleteCase,
|
||||||
onDeleteCases,
|
onDeleteCases,
|
||||||
onCsvDownload,
|
onExportCases,
|
||||||
messages,
|
messages,
|
||||||
priorityMessages,
|
priorityMessages,
|
||||||
locale,
|
locale,
|
||||||
@@ -58,6 +59,7 @@ export default function TestCaseTable({
|
|||||||
column: 'id',
|
column: 'id',
|
||||||
direction: 'ascending',
|
direction: 'ascending',
|
||||||
});
|
});
|
||||||
|
const [exportType, setExportType] = useState(new Set(['json']));
|
||||||
|
|
||||||
const sortedItems = useMemo(() => {
|
const sortedItems = useMemo(() => {
|
||||||
if (cases.length === 0) {
|
if (cases.length === 0) {
|
||||||
@@ -133,6 +135,10 @@ export default function TestCaseTable({
|
|||||||
setSelectedKeys(new Set([]));
|
setSelectedKeys(new Set([]));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExportTypeChange = (keys: Selection) => {
|
||||||
|
setExportType(new Set(Array.from(keys as Set<string>)));
|
||||||
|
};
|
||||||
|
|
||||||
const classNames = useMemo(
|
const classNames = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
wrapper: ['max-w-3xl'],
|
wrapper: ['max-w-3xl'],
|
||||||
@@ -170,16 +176,38 @@ export default function TestCaseTable({
|
|||||||
{messages.delete}
|
{messages.delete}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<ButtonGroup className="me-2">
|
||||||
startContent={<Download size={16} />}
|
<Button
|
||||||
size="sm"
|
startContent={<FileDown size={16} />}
|
||||||
variant="bordered"
|
size="sm"
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
className="me-2"
|
onPress={() => onExportCases(Array.from(exportType)[0])}
|
||||||
onPress={onCsvDownload}
|
>
|
||||||
>
|
{messages.export} {exportType}
|
||||||
{messages.downloadCsv}
|
</Button>
|
||||||
</Button>
|
<Dropdown placement="bottom-end">
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button isIconOnly size="sm">
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu
|
||||||
|
disallowEmptySelection
|
||||||
|
aria-label="Export options"
|
||||||
|
className="max-w-[300px]"
|
||||||
|
selectedKeys={exportType}
|
||||||
|
selectionMode="single"
|
||||||
|
onSelectionChange={handleExportTypeChange}
|
||||||
|
>
|
||||||
|
<DropdownItem key="json" startContent={<FileJson size={16} />}>
|
||||||
|
json
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem key="csv" startContent={<FileSpreadsheet size={16} />}>
|
||||||
|
csv
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</ButtonGroup>
|
||||||
<Button
|
<Button
|
||||||
startContent={<Plus size={16} />}
|
startContent={<Plus size={16} />}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect, useContext, ChangeEvent, DragEvent } from 'react';
|
import { useState, useEffect, useContext, ChangeEvent, DragEvent } from 'react';
|
||||||
import { Input, Textarea, Select, SelectItem, Button, Divider, Tooltip, addToast } from '@heroui/react';
|
import { Input, Textarea, Select, SelectItem, Button, Divider, Tooltip, addToast, Badge } from '@heroui/react';
|
||||||
import { useRouter } from '@/src/i18n/routing';
|
import { useRouter } from '@/src/i18n/routing';
|
||||||
import { Save, Plus, ArrowLeft, Circle } from 'lucide-react';
|
import { Save, Plus, ArrowLeft, Circle } from 'lucide-react';
|
||||||
import { priorities, testTypes, templates } from '@/config/selection';
|
import { priorities, testTypes, templates } from '@/config/selection';
|
||||||
@@ -54,7 +54,7 @@ export default function CaseEditor({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const tokenContext = useContext(TokenContext);
|
const tokenContext = useContext(TokenContext);
|
||||||
const [testCase, setTestCase] = useState<CaseType>(defaultTestCase);
|
const [testCase, setTestCase] = useState<CaseType>(defaultTestCase);
|
||||||
const [isTitleInvalid, setIsTitleInvalid] = useState<boolean>(false);
|
const [isTitleInvalid] = useState<boolean>(false);
|
||||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||||
const [plusCount, setPlusCount] = useState<number>(0);
|
const [plusCount, setPlusCount] = useState<number>(0);
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
@@ -245,9 +245,12 @@ export default function CaseEditor({
|
|||||||
<h3 className="font-bold ms-2">{testCase.title}</h3>
|
<h3 className="font-bold ms-2">{testCase.title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{isDirty && <Circle size={8} color="#525252" fill="#525252" className="me-1" />}
|
|
||||||
<Button
|
<Button
|
||||||
startContent={<Save size={16} />}
|
startContent={
|
||||||
|
<Badge isInvisible={!isDirty} color="danger" size="sm" content="" shape="circle">
|
||||||
|
<Save size={16} />
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={!tokenContext.isProjectDeveloper(Number(projectId))}
|
isDisabled={!tokenContext.isProjectDeveloper(Number(projectId))}
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function Page({ params }: { params: { projectId: string; folderId
|
|||||||
close: t('close'),
|
close: t('close'),
|
||||||
areYouSure: t('are_you_sure'),
|
areYouSure: t('are_you_sure'),
|
||||||
newTestCase: t('new_test_case'),
|
newTestCase: t('new_test_case'),
|
||||||
downloadCsv: t('download_csv'),
|
export: t('export'),
|
||||||
status: t('status'),
|
status: t('status'),
|
||||||
noCasesFound: t('no_cases_found'),
|
noCasesFound: t('no_cases_found'),
|
||||||
caseTitle: t('case_title'),
|
caseTitle: t('case_title'),
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { useEffect, useState, useContext } from 'react';
|
|||||||
import { Button } from '@heroui/react';
|
import { Button } from '@heroui/react';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import RunsTable from './RunsTable';
|
import RunsTable from './RunsTable';
|
||||||
|
import RunDialog from './RunDialog';
|
||||||
import { fetchRuns, createRun, updateRun, deleteRun } from './runsControl';
|
import { fetchRuns, createRun, updateRun, deleteRun } from './runsControl';
|
||||||
import { RunType, RunsMessages } from '@/types/run';
|
import { RunType, RunsMessages } from '@/types/run';
|
||||||
import RunDialog from './RunDialog';
|
|
||||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
||||||
import { TokenContext } from '@/utils/TokenProvider';
|
import { TokenContext } from '@/utils/TokenProvider';
|
||||||
import { LocaleCodeType } from '@/types/locale';
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
|
||||||
import { useState, useEffect, useContext } from 'react';
|
import { useState, useEffect, useContext } from 'react';
|
||||||
import { useRouter } from '@/src/i18n/routing';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
@@ -18,14 +16,23 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
addToast,
|
addToast,
|
||||||
|
ButtonGroup,
|
||||||
|
Badge,
|
||||||
} from '@heroui/react';
|
} from '@heroui/react';
|
||||||
import { Save, Circle, ArrowLeft, Folder, ChevronDown, CopyPlus, CopyMinus, RotateCw } from 'lucide-react';
|
import {
|
||||||
import RunProgressChart from './RunPregressDonutChart';
|
Save,
|
||||||
import TestCaseSelector from './TestCaseSelector';
|
ArrowLeft,
|
||||||
import { testRunStatus } from '@/config/selection';
|
Folder,
|
||||||
import { RunType, RunStatusCountType, RunMessages } from '@/types/run';
|
ChevronDown,
|
||||||
import { CaseType } from '@/types/case';
|
CopyPlus,
|
||||||
import { FolderType } from '@/types/folder';
|
CopyMinus,
|
||||||
|
RotateCw,
|
||||||
|
FileDown,
|
||||||
|
FileSpreadsheet,
|
||||||
|
FileCode,
|
||||||
|
FileJson,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
import {
|
import {
|
||||||
fetchRun,
|
fetchRun,
|
||||||
updateRun,
|
updateRun,
|
||||||
@@ -33,10 +40,17 @@ import {
|
|||||||
fetchProjectCases,
|
fetchProjectCases,
|
||||||
includeExcludeTestCases,
|
includeExcludeTestCases,
|
||||||
changeStatus,
|
changeStatus,
|
||||||
|
exportRun,
|
||||||
} from '../runsControl';
|
} from '../runsControl';
|
||||||
import { fetchFolders } from '../../folders/foldersControl';
|
import { fetchFolders } from '../../folders/foldersControl';
|
||||||
|
import RunProgressChart from './RunPregressDonutChart';
|
||||||
|
import TestCaseSelector from './TestCaseSelector';
|
||||||
|
import { useRouter } from '@/src/i18n/routing';
|
||||||
|
import { testRunStatus } from '@/config/selection';
|
||||||
|
import { RunType, RunStatusCountType, RunMessages } from '@/types/run';
|
||||||
|
import { CaseType } from '@/types/case';
|
||||||
|
import { FolderType } from '@/types/folder';
|
||||||
import { TokenContext } from '@/utils/TokenProvider';
|
import { TokenContext } from '@/utils/TokenProvider';
|
||||||
import { useTheme } from 'next-themes';
|
|
||||||
import { useFormGuard } from '@/utils/formGuard';
|
import { useFormGuard } from '@/utils/formGuard';
|
||||||
import { PriorityMessages } from '@/types/priority';
|
import { PriorityMessages } from '@/types/priority';
|
||||||
import { RunStatusMessages, TestRunCaseStatusMessages } from '@/types/status';
|
import { RunStatusMessages, TestRunCaseStatusMessages } from '@/types/status';
|
||||||
@@ -75,7 +89,7 @@ export default function RunEditor({
|
|||||||
locale,
|
locale,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const tokenContext = useContext(TokenContext);
|
const tokenContext = useContext(TokenContext);
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [testRun, setTestRun] = useState<RunType>(defaultTestRun);
|
const [testRun, setTestRun] = useState<RunType>(defaultTestRun);
|
||||||
const [folders, setFolders] = useState<FolderType[]>([]);
|
const [folders, setFolders] = useState<FolderType[]>([]);
|
||||||
const [runStatusCounts, setRunStatusCounts] = useState<RunStatusCountType[]>([]);
|
const [runStatusCounts, setRunStatusCounts] = useState<RunStatusCountType[]>([]);
|
||||||
@@ -83,9 +97,10 @@ export default function RunEditor({
|
|||||||
const [selectedFolder, setSelectedFolder] = useState<FolderType | null>(null);
|
const [selectedFolder, setSelectedFolder] = useState<FolderType | null>(null);
|
||||||
const [testCases, setTestCases] = useState<CaseType[]>([]);
|
const [testCases, setTestCases] = useState<CaseType[]>([]);
|
||||||
const [filteredTestCases, setFilteredTestCases] = useState<CaseType[]>([]);
|
const [filteredTestCases, setFilteredTestCases] = useState<CaseType[]>([]);
|
||||||
const [isNameInvalid, setIsNameInvalid] = useState<boolean>(false);
|
const [isNameInvalid] = useState<boolean>(false);
|
||||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
|
const [exportType, setExportType] = useState(new Set(['xml']));
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
useFormGuard(isDirty, messages.areYouSureLeave);
|
useFormGuard(isDirty, messages.areYouSureLeave);
|
||||||
|
|
||||||
@@ -184,6 +199,10 @@ export default function RunEditor({
|
|||||||
const baseClass = '';
|
const baseClass = '';
|
||||||
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
||||||
|
|
||||||
|
const handleExportTypeChange = (keys: Selection) => {
|
||||||
|
setExportType(new Set(Array.from(keys as Set<string>)));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
|
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
|
||||||
@@ -201,9 +220,47 @@ export default function RunEditor({
|
|||||||
<h3 className="font-bold ms-2">{testRun.name}</h3>
|
<h3 className="font-bold ms-2">{testRun.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{isDirty && <Circle size={8} color="#525252" fill="#525252" className="me-1" />}
|
<ButtonGroup className="me-2">
|
||||||
|
<Button
|
||||||
|
startContent={<FileDown size={16} />}
|
||||||
|
size="sm"
|
||||||
|
isDisabled={!tokenContext.isProjectReporter(Number(projectId))}
|
||||||
|
onPress={() => exportRun(tokenContext.token.access_token, Number(testRun.id), Array.from(exportType)[0])}
|
||||||
|
>
|
||||||
|
{messages.export} {exportType}
|
||||||
|
</Button>
|
||||||
|
<Dropdown placement="bottom-end">
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button isIconOnly size="sm">
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu
|
||||||
|
disallowEmptySelection
|
||||||
|
aria-label="Export options"
|
||||||
|
className="max-w-[300px]"
|
||||||
|
selectedKeys={exportType}
|
||||||
|
selectionMode="single"
|
||||||
|
onSelectionChange={handleExportTypeChange}
|
||||||
|
>
|
||||||
|
<DropdownItem key="xml" startContent={<FileCode size={16} />}>
|
||||||
|
xml
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem key="json" startContent={<FileJson size={16} />}>
|
||||||
|
json
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem key="csv" startContent={<FileSpreadsheet size={16} />}>
|
||||||
|
csv
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</ButtonGroup>
|
||||||
<Button
|
<Button
|
||||||
startContent={<Save size={16} />}
|
startContent={
|
||||||
|
<Badge isInvisible={!isDirty} color="danger" size="sm" content="" shape="circle">
|
||||||
|
<Save size={16} />
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={!tokenContext.isProjectReporter(Number(projectId))}
|
isDisabled={!tokenContext.isProjectReporter(Number(projectId))}
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default function Page({ params }: { params: { projectId: string; runId: s
|
|||||||
updating: t('updating'),
|
updating: t('updating'),
|
||||||
update: t('update'),
|
update: t('update'),
|
||||||
updatedTestRun: t('updated_test_run'),
|
updatedTestRun: t('updated_test_run'),
|
||||||
|
export: t('export'),
|
||||||
progress: t('progress'),
|
progress: t('progress'),
|
||||||
refresh: t('refresh'),
|
refresh: t('refresh'),
|
||||||
id: t('id'),
|
id: t('id'),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LocaleCodeType } from '@/types/locale';
|
|
||||||
import RunsPage from './RunsPage';
|
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import RunsPage from './RunsPage';
|
||||||
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
|
||||||
export async function generateMetadata({ params: { locale } }: { params: { locale: LocaleCodeType } }) {
|
export async function generateMetadata({ params: { locale } }: { params: { locale: LocaleCodeType } }) {
|
||||||
const t = await getTranslations({ locale, namespace: 'Runs' });
|
const t = await getTranslations({ locale, namespace: 'Runs' });
|
||||||
|
|||||||
@@ -129,6 +129,39 @@ async function deleteRun(jwt: string, runId: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exportRun(jwt: string, runId: number, type: string) {
|
||||||
|
if (type !== 'xml' && type !== 'json' && type !== 'csv') {
|
||||||
|
console.error('export type error. type:', type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `${apiServer}/runs/download/${runId}?type=${type}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const objectUrl = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = objectUrl;
|
||||||
|
a.download = `run_${runId}.${type}`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
window.URL.revokeObjectURL(objectUrl);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching data:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchRunCases(jwt: string, runId: number) {
|
async function fetchRunCases(jwt: string, runId: number) {
|
||||||
const url = `${apiServer}/runcases?runId=${runId}`;
|
const url = `${apiServer}/runcases?runId=${runId}`;
|
||||||
|
|
||||||
@@ -310,6 +343,7 @@ export {
|
|||||||
createRun,
|
createRun,
|
||||||
updateRun,
|
updateRun,
|
||||||
deleteRun,
|
deleteRun,
|
||||||
|
exportRun,
|
||||||
fetchRunCases,
|
fetchRunCases,
|
||||||
changeStatus,
|
changeStatus,
|
||||||
includeExcludeTestCases,
|
includeExcludeTestCases,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ type CasesMessages = {
|
|||||||
areYouSure: string;
|
areYouSure: string;
|
||||||
delete: string;
|
delete: string;
|
||||||
newTestCase: string;
|
newTestCase: string;
|
||||||
downloadCsv: string;
|
export: string;
|
||||||
status: string;
|
status: string;
|
||||||
noCasesFound: string;
|
noCasesFound: string;
|
||||||
caseTitle: string;
|
caseTitle: string;
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ type RunMessages = {
|
|||||||
updating: string;
|
updating: string;
|
||||||
update: string;
|
update: string;
|
||||||
updatedTestRun: string;
|
updatedTestRun: string;
|
||||||
|
export: string;
|
||||||
progress: string;
|
progress: string;
|
||||||
refresh: string;
|
refresh: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -132,8 +132,12 @@ async function deleteCases(jwt: string, deleteCaseIds: number[], projectId: numb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function csvDownload(jwt: string, folderId: number) {
|
async function exportCases(jwt: string, folderId: number, type: string) {
|
||||||
const url = `${apiServer}/cases/download?folderId=${folderId}&type=csv`;
|
if (type !== 'json' && type !== 'csv') {
|
||||||
|
console.error('export type error. type:', type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `${apiServer}/cases/download?folderId=${folderId}&type=${type}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -151,7 +155,7 @@ async function csvDownload(jwt: string, folderId: number) {
|
|||||||
const objectUrl = window.URL.createObjectURL(blob);
|
const objectUrl = window.URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = objectUrl;
|
a.href = objectUrl;
|
||||||
a.download = `folder_${folderId}.csv`;
|
a.download = `folder_${folderId}.${type}`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
a.remove();
|
a.remove();
|
||||||
@@ -161,4 +165,4 @@ async function csvDownload(jwt: string, folderId: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { fetchCase, fetchCases, updateCase, createCase, deleteCases, csvDownload };
|
export { fetchCase, fetchCases, updateCase, createCase, deleteCases, exportCases };
|
||||||
|
|||||||
Reference in New Issue
Block a user