mirror of
https://github.com/usebruno/bruno.git
synced 2025-08-26 08:25:35 +02:00
filename support for requests and folders (#4111)
This commit is contained in:
296
package-lock.json
generated
296
package-lock.json
generated
@@ -359,28 +359,6 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/core/node_modules/fast-xml-parser": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz",
|
||||
"integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/naturalintelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strnum": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-env": {
|
||||
"version": "3.654.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.654.0.tgz",
|
||||
@@ -1355,28 +1333,6 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/nested-clients/node_modules/fast-xml-parser": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz",
|
||||
"integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/naturalintelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strnum": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/region-config-resolver": {
|
||||
"version": "3.654.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.654.0.tgz",
|
||||
@@ -1841,24 +1797,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz",
|
||||
"integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
|
||||
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.26.9",
|
||||
"@babel/types": "^7.26.10"
|
||||
"@babel/template": "^7.25.9",
|
||||
"@babel/types": "^7.26.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz",
|
||||
"integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
|
||||
"version": "7.26.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
|
||||
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.10"
|
||||
"@babel/types": "^7.26.3"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -3185,9 +3143,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
|
||||
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
|
||||
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@@ -3196,13 +3155,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
|
||||
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
||||
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/parser": "^7.26.9",
|
||||
"@babel/types": "^7.26.9"
|
||||
"@babel/code-frame": "^7.25.9",
|
||||
"@babel/parser": "^7.25.9",
|
||||
"@babel/types": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -3250,9 +3210,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
|
||||
"integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
|
||||
"version": "7.26.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
|
||||
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9"
|
||||
@@ -8904,9 +8865,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
|
||||
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
|
||||
"integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -8925,6 +8887,17 @@
|
||||
"js-md4": "^0.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-ntlm/node_modules/axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||
@@ -10996,6 +10969,20 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/config-file-ts/node_modules/typescript": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/console-browserify": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
|
||||
@@ -13147,6 +13134,28 @@
|
||||
"integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz",
|
||||
"integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/naturalintelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strnum": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/fastest-levenshtein": {
|
||||
"version": "1.0.16",
|
||||
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
|
||||
@@ -24155,16 +24164,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
@@ -25122,6 +25132,7 @@
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz",
|
||||
"integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jsep-plugin/assignment": "^1.3.0",
|
||||
"@jsep-plugin/regex": "^1.0.4",
|
||||
@@ -26223,26 +26234,15 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-cli/node_modules/fast-xml-parser": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz",
|
||||
"integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/naturalintelligence"
|
||||
}
|
||||
],
|
||||
"packages/bruno-cli/node_modules/axios": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
|
||||
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strnum": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-cli/node_modules/fs-extra": {
|
||||
@@ -26274,19 +26274,6 @@
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron": {
|
||||
"name": "bruno",
|
||||
"version": "v1.38.1",
|
||||
@@ -27364,26 +27351,15 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/fast-xml-parser": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz",
|
||||
"integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/naturalintelligence"
|
||||
}
|
||||
],
|
||||
"packages/bruno-electron/node_modules/axios": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
|
||||
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strnum": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/fs-extra": {
|
||||
@@ -27469,19 +27445,6 @@
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-graphql-docs/node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js": {
|
||||
"name": "@usebruno/js",
|
||||
"version": "0.12.0",
|
||||
@@ -27522,6 +27485,17 @@
|
||||
"@usebruno/vm2": "^3.9.13"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/axios": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
|
||||
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
@@ -27566,19 +27540,6 @@
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-query/node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-schema": {
|
||||
"name": "@usebruno/schema",
|
||||
"version": "0.7.0",
|
||||
@@ -27626,10 +27587,22 @@
|
||||
"multer": "^1.4.5-lts.1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/axios": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
|
||||
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -27638,6 +27611,7 @@
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -27680,22 +27654,18 @@
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/fast-xml-parser": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz",
|
||||
"integrity": "sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w==",
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.0.9.tgz",
|
||||
"integrity": "sha512-2mBwCiuW3ycKQQ6SOesSB8WeF+fIGb6I/GG5vU5/XEptwFFhp9PE8b9O7fbs2dpq9fXn4ULR3UsfydNUCntf5A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/naturalintelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strnum": "^1.0.5"
|
||||
"strnum": "^2.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
@@ -27704,12 +27674,14 @@
|
||||
"packages/bruno-tests/node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-tests/node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
@@ -27720,6 +27692,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/strnum": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz",
|
||||
"integrity": "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-toml": {
|
||||
"name": "@usebruno/toml",
|
||||
"version": "0.1.0",
|
||||
|
@@ -8,9 +8,7 @@ import { useState } from 'react';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useRef } from 'react';
|
||||
import path from 'path';
|
||||
import slash from 'utils/common/slash';
|
||||
import { isWindowsOS } from 'utils/common/platform';
|
||||
import path from 'utils/common/path';
|
||||
|
||||
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
const certFilePathInputRef = useRef();
|
||||
@@ -70,12 +68,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
const getFile = (e) => {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
|
||||
if (filePath) {
|
||||
let relativePath;
|
||||
if (isWindowsOS()) {
|
||||
relativePath = slash(path.win32.relative(root, filePath));
|
||||
} else {
|
||||
relativePath = path.posix.relative(root, filePath);
|
||||
}
|
||||
let relativePath = path.relative(root, filePath);
|
||||
formik.setFieldValue(e.name, relativePath);
|
||||
}
|
||||
};
|
||||
@@ -109,8 +102,8 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<ul className="mt-4">
|
||||
{!clientCertConfig.length
|
||||
? 'No client certificates added'
|
||||
: clientCertConfig.map((clientCert) => (
|
||||
<li key={uuid()} className="flex items-center available-certificates p-2 rounded-lg mb-2">
|
||||
: clientCertConfig.map((clientCert, index) => (
|
||||
<li key={`client-cert-${index}`} className="flex items-center available-certificates p-2 rounded-lg mb-2">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<div className="flex w-full items-center">
|
||||
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
|
||||
@@ -198,9 +191,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<div
|
||||
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
|
||||
title={path.basename(slash(formik.values.certFilePath))}
|
||||
title={path.basename(formik.values.certFilePath)}
|
||||
>
|
||||
{path.basename(slash(formik.values.certFilePath))}
|
||||
{path.basename(formik.values.certFilePath)}
|
||||
</div>
|
||||
<IconTrash
|
||||
size={18}
|
||||
@@ -238,9 +231,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<div
|
||||
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
|
||||
title={path.basename(slash(formik.values.keyFilePath))}
|
||||
title={path.basename(formik.values.keyFilePath)}
|
||||
>
|
||||
{path.basename(slash(formik.values.keyFilePath))}
|
||||
{path.basename(formik.values.keyFilePath)}
|
||||
</div>
|
||||
<IconTrash
|
||||
size={18}
|
||||
@@ -281,9 +274,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<div
|
||||
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
|
||||
title={path.basename(slash(formik.values.pfxFilePath))}
|
||||
title={path.basename(formik.values.pfxFilePath)}
|
||||
>
|
||||
{path.basename(slash(formik.values.pfxFilePath))}
|
||||
{path.basename(formik.values.pfxFilePath)}
|
||||
</div>
|
||||
<IconTrash
|
||||
size={18}
|
||||
|
@@ -6,6 +6,7 @@ import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const CreateEnvironment = ({ collection, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -23,7 +24,11 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'Must be at least 1 character')
|
||||
.max(50, 'Must be 50 characters or less')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('Name is required')
|
||||
.test('duplicate-name', 'Environment already exists', validateEnvironmentName)
|
||||
}),
|
||||
|
@@ -6,6 +6,7 @@ import { useFormik } from 'formik';
|
||||
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -18,7 +19,11 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import path from 'path';
|
||||
import path from 'utils/common/path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconX } from '@tabler/icons';
|
||||
import { isWindowsOS } from 'utils/common/platform';
|
||||
import slash from 'utils/common/slash';
|
||||
|
||||
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -27,7 +26,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
|
||||
const collectionDir = collection.pathname;
|
||||
|
||||
if (filePath.startsWith(collectionDir)) {
|
||||
return path.relative(slash(collectionDir), slash(filePath));
|
||||
return path.relative(collectionDir, filePath);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
|
@@ -6,6 +6,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const CreateEnvironment = ({ onClose }) => {
|
||||
const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);
|
||||
@@ -25,7 +26,11 @@ const CreateEnvironment = ({ onClose }) => {
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'Must be at least 1 character')
|
||||
.max(50, 'Must be 50 characters or less')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('Name is required')
|
||||
.test('duplicate-name', 'Global Environment already exists', validateEnvironmentName)
|
||||
}),
|
||||
|
@@ -3,10 +3,10 @@ import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal/index';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { renameGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const RenameEnvironment = ({ onClose, environment }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -19,7 +19,11 @@ const RenameEnvironment = ({ onClose, environment }) => {
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
|
@@ -0,0 +1,22 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.path-display {
|
||||
background: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
|
||||
.filename {
|
||||
color: ${(props) => props.theme.brand};
|
||||
font-weight: 500;
|
||||
min-height: 1.25rem;
|
||||
}
|
||||
|
||||
.file-extension {
|
||||
color: ${(props) => props.theme.text};
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
58
packages/bruno-app/src/components/PathDisplay/index.js
Normal file
58
packages/bruno-app/src/components/PathDisplay/index.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { IconEdit, IconFolder, IconFile } from '@tabler/icons';
|
||||
import path from 'utils/common/path';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const PathDisplay = ({
|
||||
collection,
|
||||
item,
|
||||
filename,
|
||||
extension = '.bru',
|
||||
showExtension = true,
|
||||
toggleEditingFilename,
|
||||
showDirectory = false
|
||||
}) => {
|
||||
const relativePath = item?.pathname && path.relative(collection?.pathname, showDirectory ? path.dirname(item?.pathname) : item?.pathname);
|
||||
const pathSegments = relativePath?.split(path.sep).filter(Boolean);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="block font-semibold">Location Path</label>
|
||||
<IconEdit
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="path-display">
|
||||
<div className="flex flex-wrap items-center gap-1 text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
{showExtension ? <IconFile size={16} className="text-gray-500" /> : <IconFolder size={16} className="text-gray-500" />}
|
||||
<span className="font-medium">{collection?.name}</span>
|
||||
</div>
|
||||
{pathSegments?.length > 0 && pathSegments?.map((segment, index) => (
|
||||
<div key={index} className="flex items-center gap-1">
|
||||
<span className="text-gray-400">/</span>
|
||||
<span>{segment}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-1">
|
||||
{collection && <span className="text-gray-400">/</span>}
|
||||
<span className="filename">
|
||||
{filename}
|
||||
{showExtension && filename?.length ? (
|
||||
<span className="file-extension">{extension}</span>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PathDisplay;
|
@@ -6,8 +6,7 @@ import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
import path from 'path';
|
||||
import slash from 'utils/common/slash';
|
||||
import path from 'utils/common/path';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
|
||||
const General = ({ close }) => {
|
||||
@@ -134,7 +133,7 @@ const General = ({ close }) => {
|
||||
className={`flex items-center mt-2 pl-6 ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
|
||||
>
|
||||
<span className="flex items-center border px-2 rounded-md">
|
||||
{path.basename(slash(formik.values.customCaCertificate.filePath))}
|
||||
{path.basename(formik.values.customCaCertificate.filePath)}
|
||||
<button
|
||||
type="button"
|
||||
tabIndex="-1"
|
||||
|
@@ -1,24 +1,19 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import path from 'path';
|
||||
import path from 'utils/common/path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder, cancelRunnerExecution } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import slash from 'utils/common/slash';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
|
||||
const getDisplayName = (fullPath, pathname, name) => {
|
||||
// convert to unix style path
|
||||
fullPath = slash(fullPath);
|
||||
pathname = slash(pathname);
|
||||
|
||||
const getDisplayName = (fullPath, pathname, name = '') => {
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
const { dir } = path.parse(relativePath);
|
||||
return [dir, name].filter(i => i).join('/');
|
||||
const { dir = '' } = path.parse(relativePath);
|
||||
return path.join(dir, name);
|
||||
};
|
||||
|
||||
export default function RunnerResults({ collection }) {
|
||||
|
@@ -5,12 +5,16 @@ import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { cloneCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import Modal from 'components/Modal';
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import PathDisplay from 'components/PathDisplay/index';
|
||||
import { useState } from 'react';
|
||||
import { IconArrowBackUp } from "@tabler/icons";
|
||||
|
||||
const CloneCollection = ({ onClose, collection }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const [isEditingFilename, toggleEditingFilename] = useState(false);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
@@ -22,12 +26,15 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
validationSchema: Yup.object({
|
||||
collectionName: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.required('collection name is required'),
|
||||
collectionFolderName: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.test('is-valid-dir-name', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('folder name is required'),
|
||||
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
|
||||
}),
|
||||
@@ -85,9 +92,7 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
if (formik.values.collectionName === formik.values.collectionFolderName) {
|
||||
formik.setFieldValue('collectionFolderName', e.target.value);
|
||||
}
|
||||
!isEditingFilename && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
|
||||
}}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
@@ -123,14 +128,20 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
|
||||
<span className="font-semibold">Folder Name</span>
|
||||
<InfoTip
|
||||
content="This folder will be created under the selected location"
|
||||
infotipId="collection-folder-name-infotip"
|
||||
/>
|
||||
{isEditingFilename ?
|
||||
<>
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="block font-semibold">
|
||||
Directory Name
|
||||
</label>
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(false)}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
id="collection-folder-name"
|
||||
type="text"
|
||||
@@ -143,6 +154,16 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionFolderName || ''}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
<PathDisplay
|
||||
filename={formik.values.collectionFolderName}
|
||||
showExtension={false}
|
||||
isEditingFilename={isEditingFilename}
|
||||
toggleEditingFilename={toggleEditingFilename}
|
||||
/>
|
||||
}
|
||||
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
|
||||
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
|
||||
) : null}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
@@ -6,24 +6,42 @@ import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isItemAFolder } from 'utils/tabs';
|
||||
import { cloneItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconArrowBackUp } from '@tabler/icons';
|
||||
import * as path from 'path';
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import PathDisplay from 'components/PathDisplay/index';
|
||||
|
||||
const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const inputRef = useRef();
|
||||
const [isEditingFilename, toggleEditingFilename] = useState(false);
|
||||
const itemName = item?.name;
|
||||
const itemType = item?.type;
|
||||
const itemFilename = item?.filename ? path.parse(item?.filename).name : '';
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: item.name
|
||||
name: itemName,
|
||||
filename: sanitizeName(itemFilename)
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.required('name is required'),
|
||||
filename: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.required('name is required')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value))
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(cloneItem(values.name, item.uid, collection.uid))
|
||||
dispatch(cloneItem(values.name, values.filename, item.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Request cloned!');
|
||||
onClose();
|
||||
@@ -44,7 +62,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="sm"
|
||||
size="md"
|
||||
title={`Clone ${isFolder ? 'Folder' : 'Request'}`}
|
||||
confirmText="Clone"
|
||||
handleConfirm={onSubmit}
|
||||
@@ -66,11 +84,58 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
onChange={e => {
|
||||
formik.setFieldValue('name', e.target.value);
|
||||
!isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value));
|
||||
}}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||
</div>
|
||||
{isEditingFilename ? (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="block font-semibold">
|
||||
{isFolder ? 'Directory' : 'File'} Name
|
||||
</label>
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<input
|
||||
id="file-name"
|
||||
type="text"
|
||||
name="filename"
|
||||
placeholder="File Name"
|
||||
className={`!pr-10 block textbox mt-2 w-full`}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.filename || ''}
|
||||
/>
|
||||
{itemType !== 'folder' && <span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
item={item}
|
||||
filename={formik.values.filename}
|
||||
showExtension={itemType !== 'folder'}
|
||||
isEditingFilename={isEditingFilename}
|
||||
toggleEditingFilename={toggleEditingFilename}
|
||||
showDirectory={true}
|
||||
/>
|
||||
)}
|
||||
{formik.touched.filename && formik.errors.filename ? (
|
||||
<div className="text-red-500">{formik.errors.filename}</div>
|
||||
) : null}
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
|
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import * as path from 'path';
|
||||
|
||||
const CollectionItemInfo = ({ collection, item, onClose }) => {
|
||||
const { pathname: collectionPathname } = collection;
|
||||
const { name, filename, pathname, type } = item;
|
||||
const relativePathname = path.relative(collectionPathname, pathname);
|
||||
return (
|
||||
<Modal
|
||||
size="md"
|
||||
title={`Info`}
|
||||
handleCancel={onClose}
|
||||
hideCancel={true}
|
||||
hideFooter={true}
|
||||
>
|
||||
<div className="w-fit flex flex-col h-full">
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right opacity-50">Name :</td>
|
||||
<td className="py-2 px-2 text-nowrap truncate max-w-[500px]" title={name}>{name}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right opacity-50">{type=='folder' ? 'Directory Name' : 'File Name'} :</td>
|
||||
<td className="py-2 px-2 break-all text-nowrap truncate max-w-[500px]" title={filename}>{filename}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right opacity-50">Pathname :</td>
|
||||
<td className="py-2 px-2 break-all text-nowrap truncate max-w-[500px]" title={relativePathname}>{relativePathname}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionItemInfo;
|
@@ -1,46 +1,73 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isItemAFolder } from 'utils/tabs';
|
||||
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import path from 'utils/common/path';
|
||||
import { IconArrowBackUp } from '@tabler/icons';
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import PathDisplay from 'components/PathDisplay';
|
||||
|
||||
const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const inputRef = useRef();
|
||||
const [isEditingFilename, toggleEditingFilename] = useState(false);
|
||||
const itemName = item?.name;
|
||||
const itemType = item?.type;
|
||||
const itemFilename = item?.filename ? path.parse(item?.filename).name : '';
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: item.name
|
||||
name: itemName,
|
||||
filename: sanitizeName(itemFilename)
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.required('name is required'),
|
||||
filename: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.required('name is required')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value))
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
// if there is unsaved changes in the request,
|
||||
// save them before renaming the request
|
||||
if ((item.name === values.name) && (itemFilename === values.filename)) {
|
||||
return;
|
||||
}
|
||||
if (!isFolder && item.draft) {
|
||||
await dispatch(saveRequest(item.uid, collection.uid, true));
|
||||
}
|
||||
if (item.name === values.name) {
|
||||
return;
|
||||
const { name: newName, filename: newFilename } = values;
|
||||
try {
|
||||
let renameConfig = {
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
};
|
||||
renameConfig['newName'] = newName;
|
||||
if (itemFilename !== newFilename) {
|
||||
renameConfig['newFilename'] = newFilename;
|
||||
}
|
||||
await dispatch(renameItem(renameConfig));
|
||||
if (isFolder) {
|
||||
dispatch(closeTabs({ tabUids: [item.uid] }));
|
||||
}
|
||||
dispatch(renameItem(values.name, item.uid, collection.uid))
|
||||
.then(() => {
|
||||
isFolder && dispatch(closeTabs({ tabUids: [item.uid] }));
|
||||
toast.success(isFolder ? 'Folder renamed' : 'Request renamed');
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err ? err.message : 'An error occurred while renaming the request');
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'An error occurred while renaming');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -54,14 +81,14 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="sm"
|
||||
size="md"
|
||||
title={`Rename ${isFolder ? 'Folder' : 'Request'}`}
|
||||
confirmText="Rename"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<form className="bruno-form" onSubmit={e => {e.preventDefault()}}>
|
||||
<div className='flex flex-col mt-2'>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
{isFolder ? 'Folder' : 'Request'} Name
|
||||
</label>
|
||||
@@ -75,11 +102,59 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
onChange={e => {
|
||||
formik.setFieldValue('name', e.target.value);
|
||||
!isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value));
|
||||
}}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||
</div>
|
||||
|
||||
{isEditingFilename ? (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="block font-semibold">
|
||||
{isFolder ? 'Directory' : 'File'} Name
|
||||
</label>
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<input
|
||||
id="file-name"
|
||||
type="text"
|
||||
name="filename"
|
||||
placeholder="File Name"
|
||||
className={`!pr-10 block textbox mt-2 w-full`}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.filename || ''}
|
||||
/>
|
||||
{itemType !== 'folder' && <span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
item={item}
|
||||
filename={formik.values.filename}
|
||||
showExtension={itemType !== 'folder'}
|
||||
isEditingFilename={isEditingFilename}
|
||||
toggleEditingFilename={toggleEditingFilename}
|
||||
showDirectory={true}
|
||||
/>
|
||||
)}
|
||||
{formik.touched.filename && formik.errors.filename ? (
|
||||
<div className="text-red-500">{formik.errors.filename}</div>
|
||||
) : null}
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
|
@@ -23,6 +23,7 @@ import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError/index';
|
||||
import CollectionItemInfo from './CollectionItemInfo/index';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import CollectionItemIcon from './CollectionItemIcon';
|
||||
import { scrollToTheActiveTab } from 'utils/tabs';
|
||||
@@ -41,7 +42,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
||||
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
|
||||
|
||||
const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false);
|
||||
const hasSearchText = searchText && searchText?.trim()?.length;
|
||||
const itemIsCollapsed = hasSearchText ? false : item.collapsed;
|
||||
|
||||
@@ -259,6 +260,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
{generateCodeItemModalOpen && (
|
||||
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
)}
|
||||
{itemInfoModalOpen && (
|
||||
<CollectionItemInfo item={item} collection={collection} onClose={() => setItemInfoModalOpen(false)} />
|
||||
)}
|
||||
<div className={itemRowClassName} ref={collectionItemRef}>
|
||||
<div className="flex items-center h-full w-full">
|
||||
{indents && indents.length
|
||||
@@ -413,6 +417,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
Settings
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="dropdown-item item-info"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setItemInfoModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Info
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -5,12 +5,16 @@ import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import Modal from 'components/Modal';
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import PathDisplay from 'components/PathDisplay/index';
|
||||
import { useState } from 'react';
|
||||
import { IconArrowBackUp } from '@tabler/icons';
|
||||
|
||||
const CreateCollection = ({ onClose }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const [isEditingFilename, toggleEditingFilename] = useState(false);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
@@ -22,12 +26,15 @@ const CreateCollection = ({ onClose }) => {
|
||||
validationSchema: Yup.object({
|
||||
collectionName: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.required('collection name is required'),
|
||||
collectionFolderName: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.test('is-valid-dir-name', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('folder name is required'),
|
||||
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
|
||||
}),
|
||||
@@ -78,9 +85,7 @@ const CreateCollection = ({ onClose }) => {
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
if (formik.values.collectionName === formik.values.collectionFolderName) {
|
||||
formik.setFieldValue('collectionFolderName', e.target.value);
|
||||
}
|
||||
!isEditingFilename && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
|
||||
}}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
@@ -116,14 +121,20 @@ const CreateCollection = ({ onClose }) => {
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
|
||||
<span className="font-semibold">Folder Name</span>
|
||||
<InfoTip
|
||||
content="This folder will be created under the selected location"
|
||||
infotipId="collection-folder-name-infotip"
|
||||
/>
|
||||
{isEditingFilename ?
|
||||
<>
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="block font-semibold">
|
||||
Directory Name
|
||||
</label>
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(false)}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
id="collection-folder-name"
|
||||
type="text"
|
||||
@@ -136,6 +147,16 @@ const CreateCollection = ({ onClose }) => {
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionFolderName || ''}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
<PathDisplay
|
||||
filename={formik.values.collectionFolderName}
|
||||
showExtension={false}
|
||||
isEditingFilename={isEditingFilename}
|
||||
toggleEditingFilename={toggleEditingFilename}
|
||||
/>
|
||||
}
|
||||
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
|
||||
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
|
||||
) : null}
|
||||
|
@@ -1,40 +1,52 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import toast from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { newFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconArrowBackUp } from '@tabler/icons';
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import PathDisplay from 'components/PathDisplay';
|
||||
|
||||
const NewFolder = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const [isEditingFilename, toggleEditingFilename] = useState(false);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
folderName: ''
|
||||
folderName: '',
|
||||
directoryName: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
folderName: Yup.string()
|
||||
.trim()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.required('name is required')
|
||||
.required('name is required'),
|
||||
directoryName: Yup.string()
|
||||
.trim()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.required('foldername is required')
|
||||
.test('is-valid-folder-name', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.test({
|
||||
name: 'folderName',
|
||||
message: 'The folder name "environments" at the root of the collection is reserved in bruno',
|
||||
test: (value) => {
|
||||
if (item && item.uid) {
|
||||
return true;
|
||||
}
|
||||
if (item?.uid) return true;
|
||||
return value && !value.trim().toLowerCase().includes('environments');
|
||||
}
|
||||
})
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(newFolder(values.folderName, collection.uid, item ? item.uid : null))
|
||||
dispatch(newFolder(values.folderName, values.directoryName, collection.uid, item ? item.uid : null))
|
||||
.then(() => {
|
||||
toast.success('New folder created!');
|
||||
onClose()
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the folder'));
|
||||
}
|
||||
@@ -49,8 +61,8 @@ const NewFolder = ({ collection, item, onClose }) => {
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="New Folder" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<Modal size="md" title="New Folder" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="folderName" className="block font-semibold">
|
||||
Folder Name
|
||||
@@ -65,13 +77,59 @@ const NewFolder = ({ collection, item, onClose }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
onChange={e => {
|
||||
formik.setFieldValue('folderName', e.target.value);
|
||||
!isEditingFilename && formik.setFieldValue('directoryName', sanitizeName(e.target.value));
|
||||
}}
|
||||
value={formik.values.folderName || ''}
|
||||
/>
|
||||
{formik.touched.folderName && formik.errors.folderName ? (
|
||||
<div className="text-red-500">{formik.errors.folderName}</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{isEditingFilename ? (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="directoryName" className="block font-semibold">
|
||||
Directory Name
|
||||
</label>
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<input
|
||||
id="file-name"
|
||||
type="text"
|
||||
name="directoryName"
|
||||
placeholder="Directory Name"
|
||||
className={`block textbox mt-2 w-full`}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.directoryName || ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
item={item}
|
||||
filename={formik.values.directoryName}
|
||||
showExtension={false}
|
||||
isEditingFilename={isEditingFilename}
|
||||
toggleEditingFilename={toggleEditingFilename}
|
||||
/>
|
||||
)}
|
||||
{formik.touched.directoryName && formik.errors.directoryName ? (
|
||||
<div className="text-red-500">{formik.errors.directoryName}</div>
|
||||
) : null}
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
|
@@ -1,45 +1,38 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
const StyledWrapper = styled.div`
|
||||
div.method-selector-container {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.border};
|
||||
border-right: none;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
|
||||
.method-selector {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
div.method-selector-container,
|
||||
div.input-container {
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
height: 2.3rem;
|
||||
}
|
||||
|
||||
div.input-container {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.border};
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
|
||||
input {
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea.curl-command {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
|
||||
@@ -47,6 +40,6 @@ const StyledWrapper = styled.div`
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
@@ -10,10 +10,12 @@ import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions'
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { getRequestFromCurlCommand } from 'utils/curl';
|
||||
import { IconArrowBackUp, IconCaretDown } from '@tabler/icons';
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import PathDisplay from 'components/PathDisplay';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -55,6 +57,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
setCurlRequestTypeDetected(type);
|
||||
};
|
||||
|
||||
const [isEditingFilename, toggleEditingFilename] = useState(false);
|
||||
|
||||
const getRequestType = (collectionPresets) => {
|
||||
if (!collectionPresets || !collectionPresets.requestType) {
|
||||
return 'http-request';
|
||||
@@ -79,6 +83,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
requestName: '',
|
||||
filename: '',
|
||||
requestType: getRequestType(collectionPresets),
|
||||
requestUrl: collectionPresets.requestUrl || '',
|
||||
requestMethod: 'GET',
|
||||
@@ -88,15 +93,18 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
requestName: Yup.string()
|
||||
.trim()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.required('name is required')
|
||||
.test({
|
||||
name: 'requestName',
|
||||
message: `The request names - collection and folder is reserved in bruno`,
|
||||
test: (value) => {
|
||||
const trimmedValue = value ? value.trim().toLowerCase() : '';
|
||||
return !['collection', 'folder'].includes(trimmedValue);
|
||||
}
|
||||
}),
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.required('name is required'),
|
||||
filename: Yup.string()
|
||||
.trim()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.required('filename is required')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value)),
|
||||
curlCommand: Yup.string().when('requestType', {
|
||||
is: (requestType) => requestType === 'from-curl',
|
||||
then: Yup.string()
|
||||
@@ -116,6 +124,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
newEphemeralHttpRequest({
|
||||
uid: uid,
|
||||
requestName: values.requestName,
|
||||
filename: values.filename,
|
||||
requestType: values.requestType,
|
||||
requestUrl: values.requestUrl,
|
||||
requestMethod: values.requestMethod,
|
||||
@@ -138,6 +147,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
dispatch(
|
||||
newHttpRequest({
|
||||
requestName: values.requestName,
|
||||
filename: values.filename,
|
||||
requestType: curlRequestTypeDetected,
|
||||
requestUrl: request.url,
|
||||
requestMethod: request.method,
|
||||
@@ -157,6 +167,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
dispatch(
|
||||
newHttpRequest({
|
||||
requestName: values.requestName,
|
||||
filename: values.filename,
|
||||
requestType: values.requestType,
|
||||
requestUrl: values.requestUrl,
|
||||
requestMethod: values.requestMethod,
|
||||
@@ -221,7 +232,16 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<form
|
||||
className="bruno-form"
|
||||
onSubmit={formik.handleSubmit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
formik.handleSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="requestName" className="block font-semibold">
|
||||
Type
|
||||
@@ -287,20 +307,64 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
onChange={e => {
|
||||
formik.setFieldValue('requestName', e.target.value);
|
||||
!isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value));
|
||||
}}
|
||||
value={formik.values.requestName || ''}
|
||||
/>
|
||||
{formik.touched.requestName && formik.errors.requestName ? (
|
||||
<div className="text-red-500">{formik.errors.requestName}</div>
|
||||
) : null}
|
||||
</div>
|
||||
{isEditingFilename ? (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="block font-semibold">
|
||||
File Name
|
||||
</label>
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<input
|
||||
id="file-name"
|
||||
type="text"
|
||||
name="filename"
|
||||
placeholder="File Name"
|
||||
className={`!pr-10 block textbox mt-2 w-full`}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.filename || ''}
|
||||
/>
|
||||
<span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
item={item}
|
||||
filename={formik.values.filename}
|
||||
isEditingFilename={isEditingFilename}
|
||||
toggleEditingFilename={toggleEditingFilename}
|
||||
/>
|
||||
)}
|
||||
{formik.touched.filename && formik.errors.filename ? (
|
||||
<div className="text-red-500">{formik.errors.filename}</div>
|
||||
) : null}
|
||||
{formik.values.requestType !== 'from-curl' ? (
|
||||
<>
|
||||
<div className="mt-4">
|
||||
<label htmlFor="request-url" className="block font-semibold">
|
||||
URL
|
||||
</label>
|
||||
|
||||
<div className="flex items-center mt-2 ">
|
||||
<div className="flex items-center h-full method-selector-container">
|
||||
<HttpMethodSelector
|
||||
|
@@ -3,8 +3,9 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import trim from 'lodash/trim';
|
||||
import path from 'path';
|
||||
import path from 'utils/common/path';
|
||||
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
import {
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
transformRequestToSaveToFilesystem
|
||||
} from 'utils/collections';
|
||||
import { uuid, waitForNextTick } from 'utils/common';
|
||||
import { PATH_SEPARATOR, getDirectoryName, isWindowsPath } from 'utils/common/platform';
|
||||
import { cancelNetworkRequest, sendNetworkRequest } from 'utils/network';
|
||||
import { callIpc } from 'utils/common/ipc';
|
||||
|
||||
@@ -45,9 +45,9 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { resolveRequestFilename } from 'utils/common/platform';
|
||||
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
||||
import slash from 'utils/common/slash';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index';
|
||||
import { sanitizeName } from 'utils/common/regex';
|
||||
|
||||
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@@ -343,7 +343,7 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay)
|
||||
});
|
||||
};
|
||||
|
||||
export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getState) => {
|
||||
export const newFolder = (folderName, directoryName, collectionUid, itemUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
@@ -355,14 +355,14 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
|
||||
if (!itemUid) {
|
||||
const folderWithSameNameExists = find(
|
||||
collection.items,
|
||||
(i) => i.type === 'folder' && trim(i.name) === trim(folderName)
|
||||
(i) => i.type === 'folder' && trim(i.filename) === trim(directoryName)
|
||||
);
|
||||
if (!folderWithSameNameExists) {
|
||||
const fullName = `${collection.pathname}${PATH_SEPARATOR}${folderName}`;
|
||||
const fullName = path.join(collection.pathname, directoryName);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:new-folder', fullName)
|
||||
.invoke('renderer:new-folder', fullName, folderName)
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
} else {
|
||||
@@ -373,14 +373,14 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
|
||||
if (currentItem) {
|
||||
const folderWithSameNameExists = find(
|
||||
currentItem.items,
|
||||
(i) => i.type === 'folder' && trim(i.name) === trim(folderName)
|
||||
(i) => i.type === 'folder' && trim(i.filename) === trim(directoryName)
|
||||
);
|
||||
if (!folderWithSameNameExists) {
|
||||
const fullName = `${currentItem.pathname}${PATH_SEPARATOR}${folderName}`;
|
||||
const fullName = path.join(currentItem.pathname, directoryName);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:new-folder', fullName)
|
||||
.invoke('renderer:new-folder', fullName, folderName)
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
} else {
|
||||
@@ -393,8 +393,7 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
|
||||
});
|
||||
};
|
||||
|
||||
// rename item
|
||||
export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getState) => {
|
||||
export const renameItem = ({ newName, newFilename, itemUid, collectionUid }) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
@@ -409,22 +408,53 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
||||
return reject(new Error('Unable to locate item'));
|
||||
}
|
||||
|
||||
const dirname = getDirectoryName(item.pathname);
|
||||
|
||||
let newPathname = '';
|
||||
if (item.type === 'folder') {
|
||||
newPathname = path.join(dirname, trim(newName));
|
||||
} else {
|
||||
const filename = resolveRequestFilename(newName);
|
||||
newPathname = path.join(dirname, filename);
|
||||
}
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:rename-item', slash(item.pathname), newPathname, newName).then(resolve).catch(reject);
|
||||
const renameName = async () => {
|
||||
return ipcRenderer.invoke('renderer:rename-item-name', { itemPath: item.pathname, newName })
|
||||
.catch((err) => {
|
||||
toast.error('Failed to rename the item name');
|
||||
console.error(err);
|
||||
throw new Error('Failed to rename the item name');
|
||||
});
|
||||
};
|
||||
|
||||
const renameFile = async () => {
|
||||
const dirname = path.dirname(item.pathname);
|
||||
let newPath = '';
|
||||
if (item.type === 'folder') {
|
||||
newPath = path.join(dirname, trim(newFilename));
|
||||
} else {
|
||||
const filename = resolveRequestFilename(newFilename);
|
||||
newPath = path.join(dirname, filename);
|
||||
}
|
||||
|
||||
return ipcRenderer.invoke('renderer:rename-item-filename', { oldPath: item.pathname, newPath, newName, newFilename })
|
||||
.catch((err) => {
|
||||
toast.error('Failed to rename the file');
|
||||
console.error(err);
|
||||
throw new Error('Failed to rename the file');
|
||||
});
|
||||
};
|
||||
|
||||
let renameOperation = null;
|
||||
if (newName) renameOperation = renameName;
|
||||
if (newFilename) renameOperation = renameFile;
|
||||
|
||||
if (!renameOperation) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
renameOperation()
|
||||
.then(() => {
|
||||
toast.success('Item renamed successfully');
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getState) => {
|
||||
export const cloneItem = (newName, newFilename, itemUid, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
@@ -443,36 +473,41 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
|
||||
const folderWithSameNameExists = find(
|
||||
parentFolder.items,
|
||||
(i) => i.type === 'folder' && trim(i.name) === trim(newName)
|
||||
(i) => i.type === 'folder' && trim(i?.filename) === trim(newFilename)
|
||||
);
|
||||
|
||||
if (folderWithSameNameExists) {
|
||||
return reject(new Error('Duplicate folder names under same parent folder are not allowed'));
|
||||
}
|
||||
|
||||
const collectionPath = `${parentFolder.pathname}${PATH_SEPARATOR}${newName}`;
|
||||
set(item, 'name', newName);
|
||||
set(item, 'filename', newFilename);
|
||||
set(item, 'root.meta.name', newName);
|
||||
|
||||
const collectionPath = path.join(parentFolder.pathname, newFilename);
|
||||
ipcRenderer.invoke('renderer:clone-folder', item, collectionPath).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
const parentItem = findParentItemInCollection(collectionCopy, itemUid);
|
||||
const filename = resolveRequestFilename(newName);
|
||||
const filename = resolveRequestFilename(newFilename);
|
||||
const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(item));
|
||||
itemToSave.name = trim(newName);
|
||||
set(itemToSave, 'name', trim(newName));
|
||||
set(itemToSave, 'filename', trim(filename));
|
||||
if (!parentItem) {
|
||||
const reqWithSameNameExists = find(
|
||||
collection.items,
|
||||
(i) => i.type !== 'folder' && trim(i.filename) === trim(filename)
|
||||
);
|
||||
if (!reqWithSameNameExists) {
|
||||
const fullName = `${collection.pathname}${PATH_SEPARATOR}${filename}`;
|
||||
const fullPathname = path.join(collection.pathname, filename);
|
||||
const { ipcRenderer } = window;
|
||||
const requestItems = filter(collection.items, (i) => i.type !== 'folder');
|
||||
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
|
||||
|
||||
itemSchema
|
||||
.validate(itemToSave)
|
||||
.then(() => ipcRenderer.invoke('renderer:new-request', fullName, itemToSave))
|
||||
.then(() => ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
|
||||
@@ -481,7 +516,7 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
uid: uuid(),
|
||||
type: 'OPEN_REQUEST',
|
||||
collectionUid,
|
||||
itemPathname: fullName
|
||||
itemPathname: fullPathname
|
||||
})
|
||||
);
|
||||
} else {
|
||||
@@ -493,8 +528,8 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
(i) => i.type !== 'folder' && trim(i.filename) === trim(filename)
|
||||
);
|
||||
if (!reqWithSameNameExists) {
|
||||
const dirname = getDirectoryName(item.pathname);
|
||||
const fullName = isWindowsPath(item.pathname) ? path.win32.join(dirname, filename) : path.join(dirname, filename);
|
||||
const dirname = path.dirname(item.pathname);
|
||||
const fullName = path.join(dirname, filename);
|
||||
const { ipcRenderer } = window;
|
||||
const requestItems = filter(parentItem.items, (i) => i.type !== 'folder');
|
||||
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
|
||||
@@ -719,7 +754,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
|
||||
};
|
||||
|
||||
export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params;
|
||||
const { requestName, filename, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
@@ -747,6 +782,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
uid: uuid(),
|
||||
type: requestType,
|
||||
name: requestName,
|
||||
filename,
|
||||
request: {
|
||||
method: requestMethod,
|
||||
url: requestUrl,
|
||||
@@ -769,20 +805,20 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
};
|
||||
|
||||
// itemUid is null when we are creating a new request at the root level
|
||||
const filename = resolveRequestFilename(requestName);
|
||||
const resolvedFilename = resolveRequestFilename(filename);
|
||||
if (!itemUid) {
|
||||
const reqWithSameNameExists = find(
|
||||
collection.items,
|
||||
(i) => i.type !== 'folder' && trim(i.filename) === trim(filename)
|
||||
(i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename)
|
||||
);
|
||||
const requestItems = filter(collection.items, (i) => i.type !== 'folder');
|
||||
item.seq = requestItems.length + 1;
|
||||
|
||||
if (!reqWithSameNameExists) {
|
||||
const fullName = `${collection.pathname}${PATH_SEPARATOR}${filename}`;
|
||||
const fullName = path.join(collection.pathname, resolvedFilename);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(() => {
|
||||
// task middleware will track this and open the new request in a new tab once request is created
|
||||
dispatch(
|
||||
insertTaskIntoQueue({
|
||||
@@ -792,6 +828,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
itemPathname: fullName
|
||||
})
|
||||
);
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
} else {
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
@@ -800,15 +838,14 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
if (currentItem) {
|
||||
const reqWithSameNameExists = find(
|
||||
currentItem.items,
|
||||
(i) => i.type !== 'folder' && trim(i.filename) === trim(filename)
|
||||
(i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename)
|
||||
);
|
||||
const requestItems = filter(currentItem.items, (i) => i.type !== 'folder');
|
||||
item.seq = requestItems.length + 1;
|
||||
if (!reqWithSameNameExists) {
|
||||
const fullName = `${currentItem.pathname}${PATH_SEPARATOR}${filename}`;
|
||||
const fullName = path.join(currentItem.pathname, resolvedFilename);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(() => {
|
||||
// task middleware will track this and open the new request in a new tab once request is created
|
||||
dispatch(
|
||||
insertTaskIntoQueue({
|
||||
@@ -818,6 +855,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
itemPathname: fullName
|
||||
})
|
||||
);
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
} else {
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
@@ -860,15 +899,17 @@ export const importEnvironment = (name, variables, collectionUid) => (dispatch,
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const sanitizedName = sanitizeName(name);
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-environment', collection.pathname, name, variables)
|
||||
.invoke('renderer:create-environment', collection.pathname, sanitizedName, variables)
|
||||
.then(
|
||||
dispatch(
|
||||
updateLastAction({
|
||||
collectionUid,
|
||||
lastAction: {
|
||||
type: 'ADD_ENVIRONMENT',
|
||||
payload: name
|
||||
payload: sanitizedName
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -891,15 +932,17 @@ export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, g
|
||||
return reject(new Error('Environment not found'));
|
||||
}
|
||||
|
||||
const sanitizedName = sanitizeName(name);
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-environment', collection.pathname, name, baseEnv.variables)
|
||||
.invoke('renderer:create-environment', collection.pathname, sanitizedName, baseEnv.variables)
|
||||
.then(
|
||||
dispatch(
|
||||
updateLastAction({
|
||||
collectionUid,
|
||||
lastAction: {
|
||||
type: 'ADD_ENVIRONMENT',
|
||||
payload: name
|
||||
payload: sanitizedName
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -923,12 +966,13 @@ export const renameEnvironment = (newName, environmentUid, collectionUid) => (di
|
||||
return reject(new Error('Environment not found'));
|
||||
}
|
||||
|
||||
const sanitizedName = sanitizeName(newName);
|
||||
const oldName = environment.name;
|
||||
environment.name = newName;
|
||||
environment.name = sanitizedName;
|
||||
|
||||
environmentSchema
|
||||
.validate(environment)
|
||||
.then(() => ipcRenderer.invoke('renderer:rename-environment', collection.pathname, oldName, newName))
|
||||
.then(() => ipcRenderer.invoke('renderer:rename-environment', collection.pathname, oldName, sanitizedName))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
|
@@ -16,10 +16,10 @@ import {
|
||||
isItemARequest
|
||||
} from 'utils/collections';
|
||||
import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
|
||||
import { getDirectoryName, getSubdirectoriesFromRoot, PATH_SEPARATOR } from 'utils/common/platform';
|
||||
import { getSubdirectoriesFromRoot } from 'utils/common/platform';
|
||||
import toast from 'react-hot-toast';
|
||||
import mime from 'mime-types';
|
||||
import path from 'node:path';
|
||||
import path from 'utils/common/path';
|
||||
|
||||
const initialState = {
|
||||
collections: [],
|
||||
@@ -1655,25 +1655,29 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
|
||||
if (isFolderRoot) {
|
||||
const folderPath = getDirectoryName(file.meta.pathname);
|
||||
const folderPath = path.dirname(file.meta.pathname);
|
||||
const folderItem = findItemInCollectionByPathname(collection, folderPath);
|
||||
if (folderItem) {
|
||||
if (file?.data?.meta?.name) {
|
||||
folderItem.name = file?.data?.meta?.name;
|
||||
}
|
||||
folderItem.root = file.data;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
const dirname = getDirectoryName(file.meta.pathname);
|
||||
const dirname = path.dirname(file.meta.pathname);
|
||||
const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname);
|
||||
let currentPath = collection.pathname;
|
||||
let currentSubItems = collection.items;
|
||||
for (const directoryName of subDirectories) {
|
||||
let childItem = currentSubItems.find((f) => f.type === 'folder' && f.name === directoryName);
|
||||
let childItem = currentSubItems.find((f) => f.type === 'folder' && f.filename === directoryName);
|
||||
currentPath = path.join(currentPath, directoryName);
|
||||
if (!childItem) {
|
||||
childItem = {
|
||||
uid: uuid(),
|
||||
pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
|
||||
pathname: currentPath,
|
||||
name: directoryName,
|
||||
collapsed: true,
|
||||
type: 'folder',
|
||||
@@ -1681,8 +1685,6 @@ export const collectionsSlice = createSlice({
|
||||
};
|
||||
currentSubItems.push(childItem);
|
||||
}
|
||||
|
||||
currentPath = `${currentPath}${PATH_SEPARATOR}${directoryName}`;
|
||||
currentSubItems = childItem.items;
|
||||
}
|
||||
|
||||
@@ -1732,20 +1734,20 @@ export const collectionsSlice = createSlice({
|
||||
let currentPath = collection.pathname;
|
||||
let currentSubItems = collection.items;
|
||||
for (const directoryName of subDirectories) {
|
||||
let childItem = currentSubItems.find((f) => f.type === 'folder' && f.name === directoryName);
|
||||
let childItem = currentSubItems.find((f) => f.type === 'folder' && f.filename === directoryName);
|
||||
currentPath = path.join(currentPath, directoryName);
|
||||
if (!childItem) {
|
||||
childItem = {
|
||||
uid: uuid(),
|
||||
pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
|
||||
name: directoryName,
|
||||
pathname: currentPath,
|
||||
name: dir?.meta?.name || directoryName,
|
||||
filename: directoryName,
|
||||
collapsed: true,
|
||||
type: 'folder',
|
||||
items: []
|
||||
};
|
||||
currentSubItems.push(childItem);
|
||||
}
|
||||
|
||||
currentPath = `${currentPath}${PATH_SEPARATOR}${directoryName}`;
|
||||
currentSubItems = childItem.items;
|
||||
}
|
||||
addDepth(collection.items);
|
||||
@@ -1753,11 +1755,25 @@ export const collectionsSlice = createSlice({
|
||||
},
|
||||
collectionChangeFileEvent: (state, action) => {
|
||||
const { file } = action.payload;
|
||||
const isCollectionRoot = file.meta.collectionRoot ? true : false;
|
||||
const isFolderRoot = file.meta.folderRoot ? true : false;
|
||||
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||
|
||||
// check and update collection root
|
||||
if (collection && file.meta.collectionRoot) {
|
||||
if (isCollectionRoot) {
|
||||
if (collection) {
|
||||
collection.root = file.data;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFolderRoot) {
|
||||
const folderPath = path.dirname(file.meta.pathname);
|
||||
const folderItem = findItemInCollectionByPathname(collection, folderPath);
|
||||
if (folderItem) {
|
||||
if (file?.data?.meta?.name) {
|
||||
folderItem.name = file?.data?.meta?.name;
|
||||
}
|
||||
folderItem.root = file.data;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';
|
||||
import { uuid } from 'utils/common';
|
||||
import path from 'path';
|
||||
import slash from 'utils/common/slash';
|
||||
import path from 'utils/common/path';
|
||||
|
||||
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
||||
if (!str || !str.length || !isString(str)) {
|
||||
@@ -90,7 +89,7 @@ export const findCollectionByItemUid = (collections, itemUid) => {
|
||||
};
|
||||
|
||||
export const findItemByPathname = (items = [], pathname) => {
|
||||
return find(items, (i) => slash(i.pathname) === slash(pathname));
|
||||
return find(items, (i) => i.pathname === pathname);
|
||||
};
|
||||
|
||||
export const findItemInCollectionByPathname = (collection, pathname) => {
|
||||
@@ -307,6 +306,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
uid: si.uid,
|
||||
type: si.type,
|
||||
name: si.name,
|
||||
filename: si.filename,
|
||||
seq: si.seq
|
||||
};
|
||||
|
||||
|
12
packages/bruno-app/src/utils/common/path.js
Normal file
12
packages/bruno-app/src/utils/common/path.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import platform from 'platform';
|
||||
import path from 'path';
|
||||
|
||||
const isWindowsOS = () => {
|
||||
const os = platform.os;
|
||||
const osFamily = os.family.toLowerCase();
|
||||
return osFamily.includes('windows');
|
||||
};
|
||||
|
||||
const brunoPath = isWindowsOS() ? path.win32 : path.posix;
|
||||
|
||||
export default brunoPath;
|
@@ -1,7 +1,6 @@
|
||||
import trim from 'lodash/trim';
|
||||
import path from 'path';
|
||||
import slash from './slash';
|
||||
import platform from 'platform';
|
||||
import path from './path';
|
||||
|
||||
export const isElectron = () => {
|
||||
if (!window) {
|
||||
@@ -16,35 +15,11 @@ export const resolveRequestFilename = (name) => {
|
||||
};
|
||||
|
||||
export const getSubdirectoriesFromRoot = (rootPath, pathname) => {
|
||||
// convert to unix style path
|
||||
pathname = slash(pathname);
|
||||
rootPath = slash(rootPath);
|
||||
|
||||
const relativePath = path.relative(rootPath, pathname);
|
||||
return relativePath ? relativePath.split(path.sep) : [];
|
||||
};
|
||||
|
||||
|
||||
export const isWindowsPath = (pathname) => {
|
||||
|
||||
if (!isWindowsOS()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for Windows drive letter format (e.g., "C:\")
|
||||
const hasDriveLetter = /^[a-zA-Z]:\\/.test(pathname);
|
||||
|
||||
// Check for UNC path format (e.g., "\\server\share") a.k.a. network path || WSL path
|
||||
const isUNCPath = pathname.startsWith('\\\\');
|
||||
|
||||
return hasDriveLetter || isUNCPath;
|
||||
};
|
||||
|
||||
|
||||
export const getDirectoryName = (pathname) => {
|
||||
return isWindowsPath(pathname) ? path.win32.dirname(pathname) : path.dirname(pathname);
|
||||
};
|
||||
|
||||
export const isWindowsOS = () => {
|
||||
const os = platform.os;
|
||||
const osFamily = os.family.toLowerCase();
|
||||
@@ -59,8 +34,6 @@ export const isMacOS = () => {
|
||||
return osFamily.includes('os x');
|
||||
};
|
||||
|
||||
export const PATH_SEPARATOR = isWindowsOS() ? '\\' : '/';
|
||||
|
||||
export const getAppInstallDate = () => {
|
||||
let dateString = localStorage.getItem('bruno.installedOn');
|
||||
|
||||
|
@@ -1 +1,55 @@
|
||||
const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; // replace invalid characters with hyphens
|
||||
const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;
|
||||
const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start
|
||||
const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters
|
||||
const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed
|
||||
|
||||
export const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
export const sanitizeName = (name) => {
|
||||
name = name
|
||||
.replace(invalidCharacters, '-') // replace invalid characters with hyphens
|
||||
.replace(/^[.\s-]+/, '') // remove leading dots, hyphens and spaces
|
||||
.replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens)
|
||||
return name;
|
||||
};
|
||||
|
||||
export const validateName = (name) => {
|
||||
if (!name) return false;
|
||||
if (name.length > 255) return false; // max name length
|
||||
|
||||
if (reservedDeviceNames.test(name)) return false; // windows reserved names
|
||||
|
||||
return (
|
||||
firstCharacter.test(name) &&
|
||||
middleCharacters.test(name) &&
|
||||
lastCharacter.test(name)
|
||||
);
|
||||
};
|
||||
|
||||
export const validateNameError = (name) => {
|
||||
if (!name) return "Name cannot be empty.";
|
||||
if (name.length > 255) {
|
||||
return "Name cannot exceed 255 characters.";
|
||||
}
|
||||
|
||||
if (reservedDeviceNames.test(name)) {
|
||||
return "Name cannot be a reserved device name.";
|
||||
}
|
||||
|
||||
if (!firstCharacter.test(name[0])) {
|
||||
return "Invalid first character.";
|
||||
}
|
||||
|
||||
for (let i = 1; i < name.length - 1; i++) {
|
||||
if (!middleCharacters.test(name[i])) {
|
||||
return `Invalid character '${name[i]}' at position ${i + 1}.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!lastCharacter.test(name[name.length - 1])) {
|
||||
return "Invalid last character.";
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
166
packages/bruno-app/src/utils/common/regex.spec.js
Normal file
166
packages/bruno-app/src/utils/common/regex.spec.js
Normal file
@@ -0,0 +1,166 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
import { sanitizeName, validateName } from './regex';
|
||||
|
||||
describe('regex validators', () => {
|
||||
describe('sanitize name', () => {
|
||||
it('should remove invalid characters', () => {
|
||||
expect(sanitizeName('hello world')).toBe('hello world');
|
||||
expect(sanitizeName('hello-world')).toBe('hello-world');
|
||||
expect(sanitizeName('hello_world')).toBe('hello_world');
|
||||
expect(sanitizeName('hello_world-')).toBe('hello_world-');
|
||||
expect(sanitizeName('hello_world-123')).toBe('hello_world-123');
|
||||
expect(sanitizeName('hello_world-123!@#$%^&*()')).toBe('hello_world-123!@#$%^&-()');
|
||||
expect(sanitizeName('hello_world?')).toBe('hello_world-');
|
||||
expect(sanitizeName('foo/bar/')).toBe('foo-bar-');
|
||||
expect(sanitizeName('foo\\bar\\')).toBe('foo-bar-');
|
||||
});
|
||||
|
||||
it('should remove leading hyphens', () => {
|
||||
expect(sanitizeName('-foo')).toBe('foo');
|
||||
expect(sanitizeName('---foo')).toBe('foo');
|
||||
expect(sanitizeName('-foo-bar')).toBe('foo-bar');
|
||||
});
|
||||
|
||||
it('should remove trailing periods', () => {
|
||||
expect(sanitizeName('.file')).toBe('file');
|
||||
expect(sanitizeName('.file.')).toBe('file');
|
||||
expect(sanitizeName('file.')).toBe('file');
|
||||
expect(sanitizeName('file.name.')).toBe('file.name');
|
||||
expect(sanitizeName('hello world.')).toBe('hello world');
|
||||
});
|
||||
|
||||
it('should handle filenames with only invalid characters', () => {
|
||||
expect(sanitizeName('<>:"/\\|?*')).toBe('');
|
||||
expect(sanitizeName('::::')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle filenames with a mix of valid and invalid characters', () => {
|
||||
expect(sanitizeName('test<>:"/\\|?*')).toBe('test---------');
|
||||
expect(sanitizeName('foo<bar>')).toBe('foo-bar-');
|
||||
});
|
||||
|
||||
it('should remove control characters', () => {
|
||||
expect(sanitizeName('foo\x00bar')).toBe('foo-bar');
|
||||
expect(sanitizeName('file\x1Fname')).toBe('file-name');
|
||||
});
|
||||
|
||||
it('should return an empty string if the name is empty or consists only of invalid characters', () => {
|
||||
expect(sanitizeName('')).toBe('');
|
||||
expect(sanitizeName('<>:"/\\|?*')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle filenames with multiple consecutive invalid characters', () => {
|
||||
expect(sanitizeName('foo<<bar')).toBe('foo--bar');
|
||||
expect(sanitizeName('test||name')).toBe('test--name');
|
||||
});
|
||||
|
||||
it('should handle names with spaces only', () => {
|
||||
expect(sanitizeName(' ')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle names with leading/trailing spaces', () => {
|
||||
expect(sanitizeName(' foo bar ')).toBe('foo bar');
|
||||
});
|
||||
|
||||
it('should preserve valid non-ASCII characters', () => {
|
||||
expect(sanitizeName('brunó')).toBe('brunó');
|
||||
expect(sanitizeName('文件')).toBe('文件');
|
||||
expect(sanitizeName('brunfais')).toBe('brunfais');
|
||||
expect(sanitizeName('brunai')).toBe('brunai');
|
||||
expect(sanitizeName('brunsборка')).toBe('brunsборка');
|
||||
expect(sanitizeName('brunпривет')).toBe('brunпривет');
|
||||
expect(sanitizeName('🐶')).toBe('🐶');
|
||||
expect(sanitizeName('brunfais🐶')).toBe('brunfais🐶');
|
||||
expect(sanitizeName('file-🐶-bruno')).toBe('file-🐶-bruno');
|
||||
expect(sanitizeName('helló')).toBe('helló');
|
||||
});
|
||||
|
||||
it('should preserve case sensitivity', () => {
|
||||
expect(sanitizeName('FileName')).toBe('FileName');
|
||||
expect(sanitizeName('fileNAME')).toBe('fileNAME');
|
||||
});
|
||||
|
||||
it('should handle filenames with multiple consecutive periods (only remove trailing)', () => {
|
||||
expect(sanitizeName('file.name...')).toBe('file.name');
|
||||
expect(sanitizeName('...file')).toBe('file');
|
||||
expect(sanitizeName('file.name... ')).toBe('file.name');
|
||||
expect(sanitizeName(' ...file')).toBe('file');
|
||||
expect(sanitizeName(' ...file ')).toBe('file');
|
||||
expect(sanitizeName(' ...file.... ')).toBe('file');
|
||||
});
|
||||
|
||||
it('should handle very long filenames', () => {
|
||||
const longName = 'a'.repeat(250) + '.txt';
|
||||
expect(sanitizeName(longName)).toBe(longName);
|
||||
});
|
||||
|
||||
it('should handle names with leading/trailing invalid characters', () => {
|
||||
expect(sanitizeName('-foo/bar-')).toBe('foo-bar-');
|
||||
expect(sanitizeName('/foo\\bar/')).toBe('foo-bar-');
|
||||
});
|
||||
|
||||
it('should handle different language unicode characters', () => {
|
||||
expect(sanitizeName('你好世界!?@#$%^&*()')).toBe('你好世界!-@#$%^&-()');
|
||||
expect(sanitizeName('こんにちは世界!?@#$%^&*()')).toBe('こんにちは世界!-@#$%^&-()');
|
||||
expect(sanitizeName('안녕하세요 세계!?@#$%^&*()')).toBe('안녕하세요 세계!-@#$%^&-()');
|
||||
expect(sanitizeName('مرحبا بالعالم!?@#$%^&*()')).toBe('مرحبا بالعالم!-@#$%^&-()');
|
||||
expect(sanitizeName('Здравствуй мир!?@#$%^&*()')).toBe('Здравствуй мир!-@#$%^&-()');
|
||||
expect(sanitizeName('नमस्ते दुनिया!?@#$%^&*()')).toBe('नमस्ते दुनिया!-@#$%^&-()');
|
||||
expect(sanitizeName('สวัสดีชาวโลก!?@#$%^&*()')).toBe('สวัสดีชาวโลก!-@#$%^&-()');
|
||||
expect(sanitizeName('γειά σου κόσμος!?@#$%^&*()')).toBe('γειά σου κόσμος!-@#$%^&-()');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeName and validateName', () => {
|
||||
it('should sanitize and then validate valid names', () => {
|
||||
const validNames = [
|
||||
'valid_filename.txt',
|
||||
' valid name ',
|
||||
' valid-name ',
|
||||
'valid<>name.txt',
|
||||
'file/with?invalid*chars'
|
||||
];
|
||||
|
||||
validNames.forEach(name => {
|
||||
const sanitized = sanitizeName(name);
|
||||
expect(validateName(sanitized)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should sanitize and then validate names with reserved device names', () => {
|
||||
const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'LPT2'];
|
||||
|
||||
reservedNames.forEach(name => {
|
||||
const sanitized = sanitizeName(name);
|
||||
expect(validateName(sanitized)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should sanitize invalid names to empty strings', () => {
|
||||
const invalidNames = [
|
||||
' <>:"/\\|?* ',
|
||||
' ... ',
|
||||
' ',
|
||||
];
|
||||
|
||||
invalidNames.forEach(name => {
|
||||
const sanitized = sanitizeName(name);
|
||||
expect(validateName(sanitized)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for reserved device names with leading/trailing spaces', () => {
|
||||
const mixedNames = [
|
||||
'AUX ',
|
||||
' COM1 '
|
||||
];
|
||||
|
||||
mixedNames.forEach(name => {
|
||||
const sanitized = sanitizeName(name);
|
||||
expect(validateName(sanitized)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
const slash = (path) => {
|
||||
const isExtendedLengthPath = /^\\\\\?\\/.test(path);
|
||||
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.replace(/\\/g, '/');
|
||||
};
|
||||
|
||||
export default slash;
|
@@ -62,7 +62,6 @@ export const updateUidsInCollection = (_collection) => {
|
||||
export const transformItemsInCollection = (collection) => {
|
||||
const transformItems = (items = []) => {
|
||||
each(items, (item) => {
|
||||
item.name = normalizeFileName(item.name);
|
||||
|
||||
if (['http', 'graphql'].includes(item.type)) {
|
||||
item.type = `${item.type}-request`;
|
||||
|
@@ -45,9 +45,8 @@ const openCollectionDialog = async (win, watcher) => {
|
||||
const { filePaths } = await dialog.showOpenDialog(win, {
|
||||
properties: ['openDirectory', 'createDirectory']
|
||||
});
|
||||
|
||||
if (filePaths && filePaths[0]) {
|
||||
const resolvedPath = normalizeAndResolvePath(filePaths[0]);
|
||||
const resolvedPath = path.resolve(filePaths[0]);
|
||||
if (isDirectory(resolvedPath)) {
|
||||
openCollection(win, watcher, resolvedPath);
|
||||
} else {
|
||||
|
@@ -2,8 +2,8 @@ const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chokidar = require('chokidar');
|
||||
const { hasBruExtension, isWSLPath, normalizeAndResolvePath, normalizeWslPath, sizeInMB } = require('../utils/filesystem');
|
||||
const { bruToEnvJson, bruToJson, bruToJsonViaWorker ,collectionBruToJson } = require('../bru');
|
||||
const { hasBruExtension, isWSLPath, normalizeAndResolvePath, sizeInMB } = require('../utils/filesystem');
|
||||
const { bruToEnvJson, bruToJson, bruToJsonViaWorker, collectionBruToJson } = require('../bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
|
||||
const { uuid } = require('../utils/common');
|
||||
@@ -319,20 +319,24 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
|
||||
}
|
||||
};
|
||||
|
||||
const addDirectory = (win, pathname, collectionUid, collectionPath) => {
|
||||
const addDirectory = async (win, pathname, collectionUid, collectionPath) => {
|
||||
const envDirectory = path.join(collectionPath, 'environments');
|
||||
|
||||
if (pathname === envDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = path.basename(pathname);
|
||||
|
||||
const directory = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname)
|
||||
name
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
win.webContents.send('main:collection-tree-updated', 'addDir', directory);
|
||||
};
|
||||
|
||||
@@ -399,6 +403,30 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (path.basename(pathname) === 'folder.bru') {
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname),
|
||||
folderRoot: true
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = await collectionBruToJson(bruContent);
|
||||
|
||||
hydrateBruCollectionFileWithUuid(file.data);
|
||||
win.webContents.send('main:collection-tree-updated', 'change', file);
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasBruExtension(pathname)) {
|
||||
try {
|
||||
const file = {
|
||||
@@ -439,18 +467,29 @@ const unlink = (win, pathname, collectionUid, collectionPath) => {
|
||||
}
|
||||
};
|
||||
|
||||
const unlinkDir = (win, pathname, collectionUid, collectionPath) => {
|
||||
const unlinkDir = async (win, pathname, collectionUid, collectionPath) => {
|
||||
const envDirectory = path.join(collectionPath, 'environments');
|
||||
|
||||
if (pathname === envDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const folderBruFilePath = path.join(pathname, `folder.bru`);
|
||||
|
||||
let name = path.basename(pathname);
|
||||
|
||||
if (fs.existsSync(folderBruFilePath)) {
|
||||
let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8');
|
||||
let folderBruData = await collectionBruToJson(folderBruFileContent);
|
||||
name = folderBruData?.meta?.name || name;
|
||||
}
|
||||
|
||||
const directory = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname)
|
||||
name
|
||||
}
|
||||
};
|
||||
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
|
||||
@@ -477,14 +516,13 @@ class Watcher {
|
||||
setTimeout(() => {
|
||||
const watcher = chokidar.watch(watchPath, {
|
||||
ignoreInitial: false,
|
||||
usePolling: watchPath.startsWith('\\\\') || forcePolling ? true : false,
|
||||
usePolling: isWSLPath(watchPath) || forcePolling ? true : false,
|
||||
ignored: (filepath) => {
|
||||
const normalizedPath = isWSLPath(filepath) ? normalizeWslPath(filepath) : normalizeAndResolvePath(filepath);
|
||||
const normalizedPath = normalizeAndResolvePath(filepath);
|
||||
const relativePath = path.relative(watchPath, normalizedPath);
|
||||
|
||||
return ignores.some((ignorePattern) => {
|
||||
const normalizedIgnorePattern = isWSLPath(ignorePattern) ? normalizeWslPath(ignorePattern) : ignorePattern.replace(/\\/g, '/');
|
||||
return relativePath === normalizedIgnorePattern || relativePath.startsWith(normalizedIgnorePattern);
|
||||
return relativePath === ignorePattern || relativePath.startsWith(ignorePattern);
|
||||
});
|
||||
},
|
||||
persistent: true,
|
||||
|
@@ -4,10 +4,9 @@ const fsExtra = require('fs-extra');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { ipcMain, shell, dialog, app } = require('electron');
|
||||
const { envJsonToBru, bruToJson, jsonToBruViaWorker, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru');
|
||||
const { envJsonToBru, bruToJson, jsonToBru, jsonToBruViaWorker, collectionBruToJson, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru');
|
||||
|
||||
const {
|
||||
isValidPathname,
|
||||
writeFile,
|
||||
hasBruExtension,
|
||||
isDirectory,
|
||||
@@ -15,16 +14,15 @@ const {
|
||||
browseFiles,
|
||||
createDirectory,
|
||||
searchForBruFiles,
|
||||
sanitizeDirectoryName,
|
||||
sanitizeName,
|
||||
isWSLPath,
|
||||
normalizeWslPath,
|
||||
normalizeAndResolvePath,
|
||||
safeToRename,
|
||||
isWindowsOS,
|
||||
isValidFilename,
|
||||
validateName,
|
||||
hasSubDirectories,
|
||||
getCollectionStats,
|
||||
sizeInMB
|
||||
sizeInMB,
|
||||
safeWriteFileSync
|
||||
} = require('../utils/filesystem');
|
||||
const { openCollectionDialog } = require('../app/collections');
|
||||
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
|
||||
@@ -74,7 +72,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
'renderer:create-collection',
|
||||
async (event, collectionName, collectionFolderName, collectionLocation) => {
|
||||
try {
|
||||
collectionFolderName = sanitizeDirectoryName(collectionFolderName);
|
||||
collectionFolderName = sanitizeName(collectionFolderName);
|
||||
const dirPath = path.join(collectionLocation, collectionFolderName);
|
||||
if (fs.existsSync(dirPath)) {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
@@ -83,7 +81,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(`collection: ${dirPath} already exists and is not empty`);
|
||||
}
|
||||
}
|
||||
if (!isValidPathname(path.basename(dirPath))) {
|
||||
|
||||
if (!validateName(path.basename(dirPath))) {
|
||||
throw new Error(`collection: invalid pathname - ${dirPath}`);
|
||||
}
|
||||
|
||||
@@ -116,13 +115,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
ipcMain.handle(
|
||||
'renderer:clone-collection',
|
||||
async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => {
|
||||
collectionFolderName = sanitizeDirectoryName(collectionFolderName);
|
||||
collectionFolderName = sanitizeName(collectionFolderName);
|
||||
const dirPath = path.join(collectionLocation, collectionFolderName);
|
||||
if (fs.existsSync(dirPath)) {
|
||||
throw new Error(`collection: ${dirPath} already exists`);
|
||||
}
|
||||
|
||||
if (!isValidPathname(path.basename(dirPath))) {
|
||||
if (!validateName(path.basename(dirPath))) {
|
||||
throw new Error(`collection: invalid pathname - ${dirPath}`);
|
||||
}
|
||||
|
||||
@@ -221,8 +220,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
if (fs.existsSync(pathname)) {
|
||||
throw new Error(`path: ${pathname} already exists`);
|
||||
}
|
||||
if (!isValidFilename(request.name)) {
|
||||
throw new Error(`path: ${request.name}.bru is not a valid filename`);
|
||||
// For the actual filename part, we want to be strict
|
||||
if (!validateName(request?.filename)) {
|
||||
throw new Error(`${request.filename}.bru is not a valid filename`);
|
||||
}
|
||||
const content = await jsonToBruViaWorker(request);
|
||||
await writeFile(pathname, content);
|
||||
@@ -358,18 +358,53 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
});
|
||||
|
||||
// rename item
|
||||
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
|
||||
const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`);
|
||||
// const parentDir = path.dirname(oldPath);
|
||||
const isWindowsOSAndNotWSLAndItemHasSubDirectories = isDirectory(oldPath) && isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath);
|
||||
// let parentDirUnwatched = false;
|
||||
// let parentDirRewatched = false;
|
||||
|
||||
ipcMain.handle('renderer:rename-item-name', async (event, { itemPath, newName }) => {
|
||||
try {
|
||||
// Normalize paths if they are WSL paths
|
||||
oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath);
|
||||
newPath = isWSLPath(newPath) ? normalizeWslPath(newPath) : normalizeAndResolvePath(newPath);
|
||||
|
||||
if (!fs.existsSync(itemPath)) {
|
||||
throw new Error(`path: ${itemPath} does not exist`);
|
||||
}
|
||||
|
||||
if (isDirectory(itemPath)) {
|
||||
const folderBruFilePath = path.join(itemPath, 'folder.bru');
|
||||
let folderBruFileJsonContent;
|
||||
if (fs.existsSync(folderBruFilePath)) {
|
||||
const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8');
|
||||
folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent);
|
||||
} else {
|
||||
folderBruFileJsonContent = {};
|
||||
}
|
||||
|
||||
folderBruFileJsonContent.meta = {
|
||||
name: newName,
|
||||
};
|
||||
|
||||
const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true);
|
||||
await writeFile(folderBruFilePath, folderBruFileContent);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isBru = hasBruExtension(itemPath);
|
||||
if (!isBru) {
|
||||
throw new Error(`path: ${itemPath} is not a bru file`);
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(itemPath, 'utf8');
|
||||
const jsonData = await bruToJson(data);
|
||||
jsonData.name = newName;
|
||||
const content = await jsonToBru(jsonData);
|
||||
await writeFile(itemPath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// rename item
|
||||
ipcMain.handle('renderer:rename-item-filename', async (event, { oldPath, newPath, newName, newFilename }) => {
|
||||
const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`);
|
||||
const isWindowsOSAndNotWSLPathAndItemHasSubDirectories = isDirectory(oldPath) && isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath);
|
||||
try {
|
||||
// Check if the old path exists
|
||||
if (!fs.existsSync(oldPath)) {
|
||||
throw new Error(`path: ${oldPath} does not exist`);
|
||||
@@ -380,6 +415,22 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
|
||||
if (isDirectory(oldPath)) {
|
||||
const folderBruFilePath = path.join(oldPath, 'folder.bru');
|
||||
let folderBruFileJsonContent;
|
||||
if (fs.existsSync(folderBruFilePath)) {
|
||||
const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8');
|
||||
folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent);
|
||||
} else {
|
||||
folderBruFileJsonContent = {};
|
||||
}
|
||||
|
||||
folderBruFileJsonContent.meta = {
|
||||
name: newName,
|
||||
};
|
||||
|
||||
const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true);
|
||||
await writeFile(folderBruFilePath, folderBruFileContent);
|
||||
|
||||
const bruFilesAtSource = await searchForBruFiles(oldPath);
|
||||
|
||||
for (let bruFile of bruFilesAtSource) {
|
||||
@@ -387,19 +438,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
moveRequestUid(bruFile, newBruFilePath);
|
||||
}
|
||||
|
||||
// watcher.unlinkItemPathInWatcher(parentDir);
|
||||
// parentDirUnwatched = true;
|
||||
|
||||
/**
|
||||
* If it is windows OS
|
||||
* And it is not WSL path (meaning it's not linux running on windows using WSL)
|
||||
* And it is not a WSL path (meaning it is not running in WSL (linux pathtype))
|
||||
* And it has sub directories
|
||||
* Only then we need to use the temp dir approach to rename the folder
|
||||
*
|
||||
* Windows OS would sometimes throw error when renaming a folder with sub directories
|
||||
* This is an alternative approach to avoid that error
|
||||
*/
|
||||
if (isWindowsOSAndNotWSLAndItemHasSubDirectories) {
|
||||
if (isWindowsOSAndNotWSLPathAndItemHasSubDirectories) {
|
||||
await fsExtra.copy(oldPath, tempDir);
|
||||
await fsExtra.remove(oldPath);
|
||||
await fsExtra.move(tempDir, newPath, { overwrite: true });
|
||||
@@ -407,8 +455,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
} else {
|
||||
await fs.renameSync(oldPath, newPath);
|
||||
}
|
||||
// watcher.addItemPathInWatcher(parentDir);
|
||||
// parentDirRewatched = true;
|
||||
|
||||
return newPath;
|
||||
}
|
||||
@@ -417,8 +463,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(`path: ${oldPath} is not a bru file`);
|
||||
}
|
||||
|
||||
if (!isValidFilename(newName)) {
|
||||
throw new Error(`path: ${newName} is not a valid filename`);
|
||||
if (!validateName(newFilename)) {
|
||||
throw new Error(`path: ${newFilename} is not a valid filename`);
|
||||
}
|
||||
|
||||
// update name in file and save new copy, then delete old copy
|
||||
@@ -433,15 +479,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
return newPath;
|
||||
} catch (error) {
|
||||
// in case an error occurs during the rename file operations after unlinking the parent dir
|
||||
// and the rewatch fails, we need to add it back to watcher
|
||||
// if (parentDirUnwatched && !parentDirRewatched) {
|
||||
// watcher.addItemPathInWatcher(parentDir);
|
||||
// }
|
||||
|
||||
// in case the rename file operations fails, and we see that the temp dir exists
|
||||
// and the old path does not exist, we need to restore the data from the temp dir to the old path
|
||||
if (isWindowsOSAndNotWSLAndItemHasSubDirectories) {
|
||||
if (isWindowsOSAndNotWSLPathAndItemHasSubDirectories) {
|
||||
if (fsExtra.pathExistsSync(tempDir) && !fsExtra.pathExistsSync(oldPath)) {
|
||||
try {
|
||||
await fsExtra.copy(tempDir, oldPath);
|
||||
@@ -457,12 +497,20 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
});
|
||||
|
||||
// new folder
|
||||
ipcMain.handle('renderer:new-folder', async (event, pathname) => {
|
||||
const resolvedFolderName = sanitizeDirectoryName(path.basename(pathname));
|
||||
ipcMain.handle('renderer:new-folder', async (event, pathname, folderName) => {
|
||||
const resolvedFolderName = sanitizeName(path.basename(pathname));
|
||||
pathname = path.join(path.dirname(pathname), resolvedFolderName);
|
||||
try {
|
||||
if (!fs.existsSync(pathname)) {
|
||||
fs.mkdirSync(pathname);
|
||||
const folderBruFilePath = path.join(pathname, 'folder.bru');
|
||||
let data = {
|
||||
meta: {
|
||||
name: folderName,
|
||||
}
|
||||
};
|
||||
const content = await jsonToCollectionBru(data, true); // isFolder flag
|
||||
await writeFile(folderBruFilePath, content);
|
||||
} else {
|
||||
return Promise.reject(new Error('The directory already exists'));
|
||||
}
|
||||
@@ -522,7 +570,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => {
|
||||
try {
|
||||
let collectionName = sanitizeDirectoryName(collection.name);
|
||||
let collectionName = sanitizeName(collection.name);
|
||||
let collectionPath = path.join(collectionLocation, collectionName);
|
||||
|
||||
if (fs.existsSync(collectionPath)) {
|
||||
@@ -533,13 +581,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
const parseCollectionItems = (items = [], currentPath) => {
|
||||
items.forEach(async (item) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.bru`);
|
||||
const content = await jsonToBruViaWorker(item);
|
||||
const filePath = path.join(currentPath, `${item.name}.bru`);
|
||||
fs.writeFileSync(filePath, content);
|
||||
const filePath = path.join(currentPath, sanitizedFilename);
|
||||
safeWriteFileSync(filePath, content);
|
||||
}
|
||||
if (item.type === 'folder') {
|
||||
item.name = sanitizeDirectoryName(item.name);
|
||||
const folderPath = path.join(currentPath, item.name);
|
||||
let sanitizedFolderName = sanitizeName(item?.filename || item?.name);
|
||||
const folderPath = path.join(currentPath, sanitizedFolderName);
|
||||
fs.mkdirSync(folderPath);
|
||||
|
||||
if (item?.root?.meta?.name) {
|
||||
@@ -548,7 +597,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
item.root,
|
||||
true // isFolder
|
||||
);
|
||||
fs.writeFileSync(folderBruFilePath, folderContent);
|
||||
safeWriteFileSync(folderBruFilePath, folderContent);
|
||||
}
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
@@ -557,8 +606,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
// Handle items of type 'js'
|
||||
if (item.type === 'js') {
|
||||
const filePath = path.join(currentPath, `${item.name}.js`);
|
||||
fs.writeFileSync(filePath, item.fileContent);
|
||||
let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.js`);
|
||||
const filePath = path.join(currentPath, sanitizedFilename);
|
||||
safeWriteFileSync(filePath, item.fileContent);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -571,8 +621,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
environments.forEach(async (env) => {
|
||||
const content = await envJsonToBru(env);
|
||||
const filePath = path.join(envDirPath, `${env.name}.bru`);
|
||||
fs.writeFileSync(filePath, content);
|
||||
let sanitizedEnvFilename = sanitizeName(`${env.name}.bru`);
|
||||
const filePath = path.join(envDirPath, sanitizedEnvFilename);
|
||||
safeWriteFileSync(filePath, content);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -631,19 +682,20 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
items.forEach(async (item) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
const content = await jsonToBruViaWorker(item);
|
||||
const filePath = path.join(currentPath, `${item.name}.bru`);
|
||||
fs.writeFileSync(filePath, content);
|
||||
const filePath = path.join(currentPath, item.filename);
|
||||
safeWriteFileSync(filePath, content);
|
||||
}
|
||||
if (item.type === 'folder') {
|
||||
const folderPath = path.join(currentPath, item.name);
|
||||
const folderPath = path.join(currentPath, item.filename);
|
||||
fs.mkdirSync(folderPath);
|
||||
|
||||
// If folder has a root element, then I should write its folder.bru file
|
||||
if (item.root) {
|
||||
const folderContent = await jsonToCollectionBru(item.root, true);
|
||||
folderContent.name = item.name;
|
||||
if (folderContent) {
|
||||
const bruFolderPath = path.join(folderPath, `folder.bru`);
|
||||
fs.writeFileSync(bruFolderPath, folderContent);
|
||||
safeWriteFileSync(bruFolderPath, folderContent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,7 +713,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
const folderContent = await jsonToCollectionBru(itemFolder.root, true);
|
||||
if (folderContent) {
|
||||
const bruFolderPath = path.join(collectionPath, `folder.bru`);
|
||||
fs.writeFileSync(bruFolderPath, folderContent);
|
||||
safeWriteFileSync(bruFolderPath, folderContent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,7 +749,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
moveRequestUid(itemPath, newItemPath);
|
||||
|
||||
fs.unlinkSync(itemPath);
|
||||
fs.writeFileSync(newItemPath, itemContent);
|
||||
safeWriteFileSync(newItemPath, itemContent);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
@@ -254,16 +254,8 @@ const hydrateRequestWithUuid = (request, pathname) => {
|
||||
return request;
|
||||
};
|
||||
|
||||
const slash = (path) => {
|
||||
const isExtendedLengthPath = /^\\\\\?\\/.test(path);
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
return path?.replace?.(/\\/g, '/');
|
||||
};
|
||||
|
||||
const findItemByPathname = (items = [], pathname) => {
|
||||
return find(items, (i) => slash(i.pathname) === slash(pathname));
|
||||
return find(items, (i) => i.pathname === pathname);
|
||||
};
|
||||
|
||||
const findItemInCollectionByPathname = (collection, pathname) => {
|
||||
@@ -280,7 +272,6 @@ module.exports = {
|
||||
flattenItems,
|
||||
findItem,
|
||||
findItemInCollection,
|
||||
slash,
|
||||
findItemByPathname,
|
||||
findItemInCollectionByPathname,
|
||||
findParentItemInCollection,
|
||||
|
@@ -44,6 +44,11 @@ const hasSubDirectories = (dir) => {
|
||||
};
|
||||
|
||||
const normalizeAndResolvePath = (pathname) => {
|
||||
|
||||
if (isWSLPath(pathname)) {
|
||||
return normalizeWSLPath(pathname);
|
||||
}
|
||||
|
||||
if (isSymbolicLink(pathname)) {
|
||||
const absPath = path.dirname(pathname);
|
||||
const targetPath = path.resolve(absPath, fs.readlinkSync(pathname));
|
||||
@@ -59,18 +64,20 @@ const normalizeAndResolvePath = (pathname) => {
|
||||
function isWSLPath(pathname) {
|
||||
// Check if the path starts with the WSL prefix
|
||||
// eg. "\\wsl.localhost\Ubuntu\home\user\bruno\collection\scripting\api\req\getHeaders.bru"
|
||||
return pathname.startsWith('/wsl.localhost/') || pathname.startsWith('\\wsl.localhost\\');
|
||||
return pathname.startsWith('\\\\') || pathname.startsWith('//') || pathname.startsWith('/wsl.localhost/') || pathname.startsWith('\\wsl.localhost');
|
||||
|
||||
}
|
||||
|
||||
function normalizeWslPath(pathname) {
|
||||
function normalizeWSLPath(pathname) {
|
||||
// Replace the WSL path prefix and convert forward slashes to backslashes
|
||||
// This is done to achieve WSL paths (linux style) to Windows UNC equivalent (Universal Naming Conversion)
|
||||
return pathname.replace(/^\/wsl.localhost/, '\\\\wsl.localhost').replace(/\//g, '\\');
|
||||
}
|
||||
|
||||
|
||||
const writeFile = async (pathname, content, isBinary = false) => {
|
||||
try {
|
||||
await fs.writeFile(pathname, content, {
|
||||
await safeWriteFile(pathname, content, {
|
||||
encoding: !isBinary ? "utf-8" : null
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -110,7 +117,7 @@ const browseDirectory = async (win) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const resolvedPath = normalizeAndResolvePath(filePaths[0]);
|
||||
const resolvedPath = path.resolve(filePaths[0]);
|
||||
return isDirectory(resolvedPath) ? resolvedPath : false;
|
||||
};
|
||||
|
||||
@@ -124,7 +131,7 @@ const browseFiles = async (win, filters = [], properties = []) => {
|
||||
return [];
|
||||
}
|
||||
|
||||
return filePaths.map((path) => normalizeAndResolvePath(path)).filter((path) => isFile(path));
|
||||
return filePaths.map((path) => path.resolve(path)).filter((path) => isFile(path));
|
||||
};
|
||||
|
||||
const chooseFileToSave = async (win, preferredFileName = '') => {
|
||||
@@ -154,28 +161,36 @@ const searchForBruFiles = (dir) => {
|
||||
return searchForFiles(dir, '.bru');
|
||||
};
|
||||
|
||||
const sanitizeDirectoryName = (name) => {
|
||||
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-').trim();
|
||||
const sanitizeName = (name) => {
|
||||
const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g;
|
||||
name = name
|
||||
.replace(invalidCharacters, '-') // replace invalid characters with hyphens
|
||||
.replace(/^[.\s]+/, '') // remove leading dots and and spaces
|
||||
.replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens)
|
||||
return name;
|
||||
};
|
||||
|
||||
const isWindowsOS = () => {
|
||||
return os.platform() === 'win32';
|
||||
}
|
||||
|
||||
const isValidFilename = (fileName) => {
|
||||
const inValidChars = /[\\/:*?"<>|]/;
|
||||
const validateName = (name) => {
|
||||
const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;
|
||||
const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start
|
||||
const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters
|
||||
const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed
|
||||
if (name.length > 255) return false; // max name length
|
||||
|
||||
if (!fileName || inValidChars.test(fileName)) {
|
||||
return false;
|
||||
}
|
||||
if (reservedDeviceNames.test(name)) return false; // windows reserved names
|
||||
|
||||
if (fileName.endsWith(' ') || fileName.endsWith('.') || fileName.startsWith('.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return (
|
||||
firstCharacter.test(name) &&
|
||||
middleCharacters.test(name) &&
|
||||
lastCharacter.test(name)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const safeToRename = (oldPath, newPath) => {
|
||||
try {
|
||||
// If the new path doesn't exist, it's safe to rename
|
||||
@@ -244,6 +259,29 @@ const sizeInMB = (size) => {
|
||||
return size / (1024 * 1024);
|
||||
}
|
||||
|
||||
const getSafePathToWrite = (filePath) => {
|
||||
const MAX_FILENAME_LENGTH = 255; // Common limit on most filesystems
|
||||
let dir = path.dirname(filePath);
|
||||
let ext = path.extname(filePath);
|
||||
let base = path.basename(filePath, ext);
|
||||
if (base.length + ext.length > MAX_FILENAME_LENGTH) {
|
||||
base = sanitizeName(base);
|
||||
base = base.slice(0, MAX_FILENAME_LENGTH - ext.length);
|
||||
}
|
||||
let safePath = path.join(dir, base + ext);
|
||||
return safePath;
|
||||
}
|
||||
|
||||
async function safeWriteFile(filePath, data, options) {
|
||||
const safePath = getSafePathToWrite(filePath);
|
||||
await fs.writeFile(safePath, data, options);
|
||||
}
|
||||
|
||||
function safeWriteFileSync(filePath, data) {
|
||||
const safePath = getSafePathToWrite(filePath);
|
||||
fs.writeFileSync(safePath, data);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isValidPathname,
|
||||
exists,
|
||||
@@ -252,7 +290,7 @@ module.exports = {
|
||||
isDirectory,
|
||||
normalizeAndResolvePath,
|
||||
isWSLPath,
|
||||
normalizeWslPath,
|
||||
normalizeWSLPath,
|
||||
writeFile,
|
||||
hasJsonExtension,
|
||||
hasBruExtension,
|
||||
@@ -262,11 +300,13 @@ module.exports = {
|
||||
chooseFileToSave,
|
||||
searchForFiles,
|
||||
searchForBruFiles,
|
||||
sanitizeDirectoryName,
|
||||
sanitizeName,
|
||||
isWindowsOS,
|
||||
safeToRename,
|
||||
isValidFilename,
|
||||
validateName,
|
||||
hasSubDirectories,
|
||||
getCollectionStats,
|
||||
sizeInMB
|
||||
sizeInMB,
|
||||
safeWriteFile,
|
||||
safeWriteFileSync
|
||||
};
|
||||
|
@@ -1,26 +1,84 @@
|
||||
const { sanitizeDirectoryName } = require('./filesystem.js');
|
||||
const { sanitizeName, isWSLPath, normalizeWSLPath, normalizeAndResolvePath } = require('./filesystem.js');
|
||||
|
||||
describe('sanitizeDirectoryName', () => {
|
||||
describe('sanitizeName', () => {
|
||||
it('should replace invalid characters with hyphens', () => {
|
||||
const input = '<>:"/\\|?*\x00-\x1F';
|
||||
const expectedOutput = '---';
|
||||
expect(sanitizeDirectoryName(input)).toEqual(expectedOutput);
|
||||
const input = '<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F';
|
||||
const expectedOutput = '----------------------------------------';
|
||||
expect(sanitizeName(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('should not modify valid directory names', () => {
|
||||
const input = 'my-directory';
|
||||
expect(sanitizeDirectoryName(input)).toEqual(input);
|
||||
expect(sanitizeName(input)).toEqual(input);
|
||||
});
|
||||
|
||||
it('should replace multiple invalid characters with a single hyphen', () => {
|
||||
const input = 'my<>invalid?directory';
|
||||
const expectedOutput = 'my-invalid-directory';
|
||||
expect(sanitizeDirectoryName(input)).toEqual(expectedOutput);
|
||||
const expectedOutput = 'my--invalid-directory';
|
||||
expect(sanitizeName(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('should handle names with slashes', () => {
|
||||
const input = 'my/invalid/directory';
|
||||
const expectedOutput = 'my-invalid-directory';
|
||||
expect(sanitizeDirectoryName(input)).toEqual(expectedOutput);
|
||||
expect(sanitizeName(input)).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WSL Path Utilities', () => {
|
||||
describe('isWSLPath', () => {
|
||||
it('should identify WSL paths starting with double backslash', () => {
|
||||
expect(isWSLPath('\\\\wsl.localhost\\Ubuntu\\home\\user')).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify WSL paths starting with double forward slash', () => {
|
||||
expect(isWSLPath('//wsl.localhost/Ubuntu/home/user')).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify WSL paths starting with /wsl.localhost/', () => {
|
||||
expect(isWSLPath('/wsl.localhost/Ubuntu/home/user')).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify WSL paths starting with \\wsl.localhost', () => {
|
||||
expect(isWSLPath('\\wsl.localhost\\Ubuntu\\home\\user')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-WSL paths', () => {
|
||||
expect(isWSLPath('C:\\Users\\user\\Documents')).toBe(false);
|
||||
expect(isWSLPath('/home/user/documents')).toBe(false);
|
||||
expect(isWSLPath('relative/path')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeWSLPath', () => {
|
||||
it('should convert forward slash WSL paths to backslash format', () => {
|
||||
const input = '/wsl.localhost/Ubuntu/home/user/file.txt';
|
||||
const expected = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt';
|
||||
expect(normalizeWSLPath(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle paths already in backslash format', () => {
|
||||
const input = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt';
|
||||
expect(normalizeWSLPath(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('should convert mixed slash formats to backslash format', () => {
|
||||
const input = '/wsl.localhost\\Ubuntu/home\\user/file.txt';
|
||||
const expected = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt';
|
||||
expect(normalizeWSLPath(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeAndResolvePath with WSL paths', () => {
|
||||
it('should normalize WSL paths', () => {
|
||||
const input = '/wsl.localhost/Ubuntu/home/user/file.txt';
|
||||
const expected = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt';
|
||||
expect(normalizeAndResolvePath(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle already normalized WSL paths', () => {
|
||||
const input = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt';
|
||||
expect(normalizeAndResolvePath(input)).toBe(input);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -189,7 +189,7 @@ const addBruShimToContext = (vm, bru) => {
|
||||
const promise = vm.newPromise();
|
||||
bru.runRequest(vm.dump(args))
|
||||
.then((response) => {
|
||||
const { status, statusText, headers, data, dataBuffer, size } = response || {};
|
||||
const { status, headers, data, dataBuffer, size, statusText } = response || {};
|
||||
promise.resolve(marshallToVm(cleanJson({ status, statusText, headers, data, dataBuffer, size }), vm));
|
||||
})
|
||||
.catch((err) => {
|
||||
|
Reference in New Issue
Block a user