Merge branch 'main' into enhancement/collection-dropdown

This commit is contained in:
game5413 2023-10-08 21:48:36 +07:00
commit 7617ae12cb
52 changed files with 1016 additions and 388 deletions

328
package-lock.json generated
View File

@ -1826,37 +1826,6 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"node_modules/@codemirror/language": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz",
"integrity": "sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==",
"peer": true,
"dependencies": {
"@codemirror/state": "^0.20.0",
"@codemirror/view": "^0.20.0",
"@lezer/common": "^0.16.0",
"@lezer/highlight": "^0.16.0",
"@lezer/lr": "^0.16.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/state": {
"version": "0.20.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz",
"integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==",
"peer": true
},
"node_modules/@codemirror/view": {
"version": "0.20.7",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz",
"integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==",
"peer": true,
"dependencies": {
"@codemirror/state": "^0.20.0",
"style-mod": "^4.0.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@ -2961,30 +2930,6 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"node_modules/@lezer/common": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz",
"integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==",
"peer": true
},
"node_modules/@lezer/highlight": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz",
"integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==",
"peer": true,
"dependencies": {
"@lezer/common": "^0.16.0"
}
},
"node_modules/@lezer/lr": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz",
"integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==",
"peer": true,
"dependencies": {
"@lezer/common": "^0.16.0"
}
},
"node_modules/@malept/cross-spawn-promise": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
@ -4839,39 +4784,6 @@
"node": ">=10.12.0"
}
},
"node_modules/autoprefixer": {
"version": "10.4.13",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz",
"integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
}
],
"peer": true,
"dependencies": {
"browserslist": "^4.21.4",
"caniuse-lite": "^1.0.30001426",
"fraction.js": "^4.2.0",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@ -5300,6 +5212,7 @@
"version": "4.21.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
"integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -7338,7 +7251,8 @@
"node_modules/electron-to-chromium": {
"version": "1.4.284",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
"integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA=="
"integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==",
"dev": true
},
"node_modules/electron-util": {
"version": "0.17.2",
@ -8140,19 +8054,6 @@
"node": ">= 0.6"
}
},
"node_modules/fraction.js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
"integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
"peer": true,
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
"url": "https://www.patreon.com/infusion"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@ -8643,18 +8544,6 @@
"node": ">= 6"
}
},
"node_modules/graphql-ws": {
"version": "5.11.2",
"resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.11.2.tgz",
"integrity": "sha512-4EiZ3/UXYcjm+xFGP544/yW1+DVI8ZpKASFbzrV5EDTFWJp0ZvLl4Dy2fSZAzz9imKp5pZMIcjB0x/H69Pv/6w==",
"peer": true,
"engines": {
"node": ">=10"
},
"peerDependencies": {
"graphql": ">=0.11 <=16"
}
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
@ -11641,7 +11530,8 @@
"node_modules/node-releases": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==",
"dev": true
},
"node_modules/normalize-package-data": {
"version": "2.5.0",
@ -11678,15 +11568,6 @@
"node": ">=0.10.0"
}
},
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
@ -13626,7 +13507,8 @@
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
"node_modules/react-redux": {
"version": "7.2.9",
@ -15018,12 +14900,6 @@
"webpack": "^5.0.0"
}
},
"node_modules/style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==",
"peer": true
},
"node_modules/styled-components": {
"version": "5.3.6",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz",
@ -15954,6 +15830,7 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
"integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -16236,12 +16113,6 @@
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
"integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA=="
},
"node_modules/w3c-keyname": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz",
"integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==",
"peer": true
},
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@ -18428,37 +18299,6 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"@codemirror/language": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz",
"integrity": "sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==",
"peer": true,
"requires": {
"@codemirror/state": "^0.20.0",
"@codemirror/view": "^0.20.0",
"@lezer/common": "^0.16.0",
"@lezer/highlight": "^0.16.0",
"@lezer/lr": "^0.16.0",
"style-mod": "^4.0.0"
}
},
"@codemirror/state": {
"version": "0.20.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz",
"integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==",
"peer": true
},
"@codemirror/view": {
"version": "0.20.7",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz",
"integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==",
"peer": true,
"requires": {
"@codemirror/state": "^0.20.0",
"style-mod": "^4.0.0",
"w3c-keyname": "^2.2.4"
}
},
"@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@ -19330,30 +19170,6 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"@lezer/common": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz",
"integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==",
"peer": true
},
"@lezer/highlight": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz",
"integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==",
"peer": true,
"requires": {
"@lezer/common": "^0.16.0"
}
},
"@lezer/lr": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz",
"integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==",
"peer": true,
"requires": {
"@lezer/common": "^0.16.0"
}
},
"@malept/cross-spawn-promise": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
@ -19680,8 +19496,7 @@
"@tabler/icons": {
"version": "1.119.0",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-1.119.0.tgz",
"integrity": "sha512-Fk3Qq4w2SXcTjc/n1cuL5bccPkylrOMo7cYpQIf/yw6zP76LQV9dtLcHQUjFiUnaYuswR645CnURIhlafyAh9g==",
"requires": {}
"integrity": "sha512-Fk3Qq4w2SXcTjc/n1cuL5bccPkylrOMo7cYpQIf/yw6zP76LQV9dtLcHQUjFiUnaYuswR645CnURIhlafyAh9g=="
},
"@tauri-apps/cli": {
"version": "1.2.2",
@ -20302,8 +20117,7 @@
}
},
"@usebruno/schema": {
"version": "file:packages/bruno-schema",
"requires": {}
"version": "file:packages/bruno-schema"
},
"@usebruno/testbench": {
"version": "file:packages/bruno-testbench",
@ -20479,8 +20293,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz",
"integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==",
"dev": true,
"requires": {}
"dev": true
},
"@webpack-cli/info": {
"version": "1.5.0",
@ -20495,8 +20308,7 @@
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz",
"integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==",
"dev": true,
"requires": {}
"dev": true
},
"@xtuc/ieee754": {
"version": "1.2.0",
@ -20601,8 +20413,7 @@
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"requires": {}
"dev": true
},
"amdefine": {
"version": "0.0.8",
@ -20935,20 +20746,6 @@
"resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz",
"integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="
},
"autoprefixer": {
"version": "10.4.13",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz",
"integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==",
"peer": true,
"requires": {
"browserslist": "^4.21.4",
"caniuse-lite": "^1.0.30001426",
"fraction.js": "^4.2.0",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0"
}
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@ -21278,6 +21075,7 @@
"version": "4.21.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
"integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001400",
"electron-to-chromium": "^1.4.251",
@ -22213,8 +22011,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz",
"integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==",
"dev": true,
"requires": {}
"dev": true
},
"css-loader": {
"version": "6.7.3",
@ -22359,8 +22156,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz",
"integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==",
"dev": true,
"requires": {}
"dev": true
},
"csso": {
"version": "4.2.0",
@ -22933,7 +22729,8 @@
"electron-to-chromium": {
"version": "1.4.284",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
"integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA=="
"integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==",
"dev": true
},
"electron-util": {
"version": "0.17.2",
@ -23558,12 +23355,6 @@
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
},
"fraction.js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
"integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
"peer": true
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@ -23826,8 +23617,7 @@
"goober": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.11.tgz",
"integrity": "sha512-5SS2lmxbhqH0u9ABEWq7WPU69a4i2pYcHeCxqaNq6Cw3mnrF0ghWNM4tEGid4dKy8XNIAUbuThuozDHHKJVh3A==",
"requires": {}
"integrity": "sha512-5SS2lmxbhqH0u9ABEWq7WPU69a4i2pYcHeCxqaNq6Cw3mnrF0ghWNM4tEGid4dKy8XNIAUbuThuozDHHKJVh3A=="
},
"got": {
"version": "9.6.0",
@ -23950,13 +23740,6 @@
}
}
},
"graphql-ws": {
"version": "5.11.2",
"resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.11.2.tgz",
"integrity": "sha512-4EiZ3/UXYcjm+xFGP544/yW1+DVI8ZpKASFbzrV5EDTFWJp0ZvLl4Dy2fSZAzz9imKp5pZMIcjB0x/H69Pv/6w==",
"peer": true,
"requires": {}
},
"handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
@ -24307,8 +24090,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
"dev": true,
"requires": {}
"dev": true
},
"idb": {
"version": "7.1.1",
@ -25087,8 +24869,7 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
"integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
"dev": true,
"requires": {}
"dev": true
},
"jest-regex-util": {
"version": "29.2.0",
@ -25850,8 +25631,7 @@
"meros": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/meros/-/meros-1.2.1.tgz",
"integrity": "sha512-R2f/jxYqCAGI19KhAvaxSOxALBMkaXWH2a7rOyqQw+ZmizX5bKkEYWLzdhC+U82ZVVPVp6MCXe3EkVligh+12g==",
"requires": {}
"integrity": "sha512-R2f/jxYqCAGI19KhAvaxSOxALBMkaXWH2a7rOyqQw+ZmizX5bKkEYWLzdhC+U82ZVVPVp6MCXe3EkVligh+12g=="
},
"methods": {
"version": "1.1.2",
@ -26185,7 +25965,8 @@
"node-releases": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==",
"dev": true
},
"normalize-package-data": {
"version": "2.5.0",
@ -26218,12 +25999,6 @@
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
},
"normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"peer": true
},
"normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
@ -26886,29 +26661,25 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz",
"integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-discard-duplicates": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz",
"integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-discard-empty": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz",
"integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-discard-overridden": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz",
"integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-js": {
"version": "3.0.3",
@ -27010,8 +26781,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-modules-local-by-default": {
"version": "4.0.0",
@ -27054,8 +26824,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz",
"integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-normalize-display-values": {
"version": "5.1.0",
@ -27576,13 +27345,13 @@
"react-inspector": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-6.0.2.tgz",
"integrity": "sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==",
"requires": {}
"integrity": "sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ=="
},
"react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
"react-redux": {
"version": "7.2.9",
@ -27769,8 +27538,7 @@
"redux-thunk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
"requires": {}
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q=="
},
"regenerate": {
"version": "1.4.2",
@ -28072,8 +27840,7 @@
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.4.tgz",
"integrity": "sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g==",
"dev": true,
"requires": {}
"dev": true
},
"rollup-plugin-postcss": {
"version": "4.0.2",
@ -28686,14 +28453,7 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz",
"integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==",
"dev": true,
"requires": {}
},
"style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==",
"peer": true
"dev": true
},
"styled-components": {
"version": "5.3.6",
@ -28730,8 +28490,7 @@
"styled-jsx": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.7.tgz",
"integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==",
"requires": {}
"integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA=="
},
"stylehacks": {
"version": "5.1.1",
@ -29380,6 +29139,7 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
"integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
"dev": true,
"requires": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
@ -29468,8 +29228,7 @@
"use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"requires": {}
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="
},
"utf8-byte-length": {
"version": "1.0.4",
@ -29602,12 +29361,6 @@
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
"integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA=="
},
"w3c-keyname": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz",
"integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==",
"peer": true
},
"walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@ -29688,8 +29441,7 @@
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
"integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
"dev": true,
"requires": {}
"dev": true
},
"schema-utils": {
"version": "3.1.1",

View File

@ -17,7 +17,7 @@ const CopyEnvironment = ({ collection, environment, onClose }) => {
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),

View File

@ -17,7 +17,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),

View File

@ -17,7 +17,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),

View File

@ -22,6 +22,7 @@ const Wrapper = styled.div`
justify-content: center;
overflow-y: auto;
z-index: 10;
background-color: rgba(0, 0, 0, 0.5);
}
.bruno-modal-card {

View File

@ -61,19 +61,20 @@ const Modal = ({
children,
confirmDisabled,
hideCancel,
hideFooter
hideFooter,
closeModalFadeTimeout = 500
}) => {
const [isClosing, setIsClosing] = useState(false);
const escFunction = (event) => {
const escKeyCode = 27;
if (event.keyCode === escKeyCode) {
closeModal();
closeModal({ type: 'esc' });
}
};
const closeModal = () => {
const closeModal = (args) => {
setIsClosing(true);
setTimeout(() => handleCancel(), 500);
setTimeout(() => handleCancel(args), closeModalFadeTimeout);
};
useEffect(() => {
@ -94,12 +95,12 @@ const Modal = ({
return (
<StyledWrapper className={classes}>
<div className={`bruno-modal-card modal-${size}`}>
<ModalHeader title={title} handleCancel={() => closeModal()} />
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} />
<ModalContent>{children}</ModalContent>
<ModalFooter
confirmText={confirmText}
cancelText={cancelText}
handleCancel={() => closeModal()}
handleCancel={() => closeModal({ type: 'button' })}
handleSubmit={handleConfirm}
confirmDisabled={confirmDisabled}
hideCancel={hideCancel}
@ -108,7 +109,12 @@ const Modal = ({
</div>
{/* Clicking on backdrop closes the modal */}
<div className="bruno-modal-backdrop" onClick={() => closeModal()} />
<div
className="bruno-modal-backdrop"
onClick={() => {
closeModal({ type: 'backdrop' });
}}
/>
</StyledWrapper>
);
};

View File

@ -20,7 +20,7 @@ const Auth = ({ item, collection }) => {
};
return (
<StyledWrapper className="w-full">
<StyledWrapper className="w-full mt-1">
<div className="flex flex-grow justify-start items-center">
<AuthMode item={item} collection={collection} />
</div>

View File

@ -40,7 +40,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
loadSchema,
isLoading: isSchemaLoading,
error: schemaError
} = useGraphqlSchema(url, environment, request, collection.collectionVariables);
} = useGraphqlSchema(url, environment, request, collection);
const loadGqlSchema = () => {
if (!isSchemaLoading) {

View File

@ -6,7 +6,7 @@ import { simpleHash } from 'utils/common';
const schemaHashPrefix = 'bruno.graphqlSchema';
const useGraphqlSchema = (endpoint, environment, request, collectionVariables) => {
const useGraphqlSchema = (endpoint, environment, request, collection) => {
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
@ -25,7 +25,7 @@ const useGraphqlSchema = (endpoint, environment, request, collectionVariables) =
const loadSchema = () => {
setIsLoading(true);
fetchGqlSchema(endpoint, environment, request, collectionVariables)
fetchGqlSchema(endpoint, environment, request, collection)
.then((res) => res.data)
.then((s) => {
if (s && s.data) {

View File

@ -32,6 +32,50 @@ const Wrapper = styled.div`
position: relative;
top: 1px;
}
.tooltip {
position: relative;
display: inline-block;
cursor: pointer;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
.tooltiptext {
visibility: hidden;
width: auto;
background-color: ${(props) => props.theme.requestTabs.active.bg};
color: ${(props) => props.theme.text};
text-align: center;
border-radius: 4px;
padding: 4px 8px;
position: absolute;
z-index: 1;
bottom: 34px;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
white-space: nowrap;
}
.tooltiptext::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -4px;
border-width: 4px;
border-style: solid;
border-color: ${(props) => props.theme.requestTabs.active.bg} transparent transparent transparent;
}
.shortcut {
font-size: 0.625rem;
}
`;
export default Wrapper;

View File

@ -5,8 +5,9 @@ import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/sli
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import HttpMethodSelector from './HttpMethodSelector';
import { useTheme } from 'providers/Theme';
import SendIcon from 'components/Icons/Send';
import { IconDeviceFloppy, IconArrowRight } from '@tabler/icons';
import SingleLineEditor from 'components/SingleLineEditor';
import { isMacOS } from 'utils/common/platform';
import StyledWrapper from './StyledWrapper';
const QueryUrl = ({ item, collection, handleRun }) => {
@ -14,6 +15,8 @@ const QueryUrl = ({ item, collection, handleRun }) => {
const dispatch = useDispatch();
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
const isMac = isMacOS();
const saveShortcut = isMac ? 'Cmd + S' : 'Ctrl + S';
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
@ -22,7 +25,10 @@ const QueryUrl = ({ item, collection, handleRun }) => {
setMethodSelectorWidth(el.offsetWidth);
}, [method]);
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const onSave = () => {
dispatch(saveRequest(item.uid, collection.uid));
};
const onUrlChange = (value) => {
dispatch(
requestUrlChanged({
@ -65,7 +71,25 @@ const QueryUrl = ({ item, collection, handleRun }) => {
collection={collection}
/>
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
<SendIcon color={theme.requestTabPanel.url.icon} width={22} />
<div
className="tooltip mr-3"
onClick={(e) => {
e.stopPropagation();
if (!item.draft) return;
onSave();
}}
>
<IconDeviceFloppy
color={item.draft ? theme.colors.text.yellow : theme.requestTabs.icon.color}
strokeWidth={1.5}
size={22}
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
/>
<span className="tooltiptext text-xs">
Save <span className="shortcut">({saveShortcut})</span>
</span>
</div>
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} />
</div>
</div>
</StyledWrapper>

View File

@ -0,0 +1,30 @@
import React from 'react';
import Modal from 'components/Modal';
const ConfirmRequestClose = ({ onCancel, onCloseWithoutSave, onSaveAndClose }) => {
const _handleCancel = ({ type }) => {
if (type === 'button') {
return onCloseWithoutSave();
}
return onCancel();
};
return (
<Modal
size="sm"
title="Unsaved changes"
confirmText="Save and Close"
cancelText="Close without saving"
handleConfirm={onSaveAndClose}
handleCancel={_handleCancel}
disableEscapeKey={true}
disableCloseOnOutsideClick={true}
closeModalFadeTimeout={150}
>
<div className="font-normal">You have unsaved changes in you request.</div>
</Modal>
);
};
export default ConfirmRequestClose;

View File

@ -1,10 +1,13 @@
import React from 'react';
import React, { useState } from 'react';
import get from 'lodash/get';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
import { findItemInCollection } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import RequestTabNotFound from './RequestTabNotFound';
import ConfirmRequestClose from './ConfirmRequestClose';
import SpecialTab from './SpecialTab';
import { useTheme } from 'providers/Theme';
import darkTheme from 'themes/dark';
@ -13,6 +16,7 @@ import lightTheme from 'themes/light';
const RequestTab = ({ tab, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [showConfirmClose, setShowConfirmClose] = useState(false);
const handleCloseClick = (event) => {
event.stopPropagation();
@ -86,6 +90,39 @@ const RequestTab = ({ tab, collection }) => {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
{showConfirmClose && (
<ConfirmRequestClose
onCancel={() => setShowConfirmClose(false)}
onCloseWithoutSave={() => {
dispatch(
deleteRequestDraft({
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
setShowConfirmClose(false);
}}
onSaveAndClose={() => {
dispatch(saveRequest(item.uid, collection.uid))
.then(() => {
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
setShowConfirmClose(false);
})
.catch((err) => {
console.log('err', err);
});
}}
/>
)}
<div className="flex items-baseline tab-label pl-2">
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
{method}
@ -94,7 +131,14 @@ const RequestTab = ({ tab, collection }) => {
{item.name}
</span>
</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
<div
className="flex px-2 close-icon-container"
onClick={(e) => {
if (!item.draft) return handleCloseClick(e);
setShowConfirmClose(true);
}}
>
{!item.draft ? (
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path

View File

@ -1,6 +1,11 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
position: absolute;
z-index: 1;
height: 100vh;
background-color: ${(props) => props.theme.requestTabPanel.responseOverlayBg};
div.overlay {
position: absolute;
top: 0;
@ -14,6 +19,11 @@ const StyledWrapper = styled.div`
padding-top: 20%;
overflow: hidden;
text-align: center;
.loading-icon {
transform: scaleY(-1);
animation: rotateCounterClockwise 1s linear infinite;
}
}
`;

View File

@ -13,17 +13,17 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
};
return (
<StyledWrapper className="mt-4 px-3 w-full">
<StyledWrapper className="px-3 w-full">
<div className="overlay">
<div style={{ marginBottom: 15, fontSize: 26 }}>
<div style={{ display: 'inline-block', fontSize: 24, marginLeft: 5, marginRight: 5 }}>
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>
<StopWatch />
</div>
</div>
<IconRefresh size={24} className="animate-spin" />
<IconRefresh size={24} className="loading-icon" />
<button
onClick={handleCancelRequest}
className="mt-4 uppercase btn-md rounded btn-secondary ease-linear transition-all duration-150"
className="mt-4 uppercase btn-sm rounded btn-secondary ease-linear transition-all duration-150"
type="button"
>
Cancel Request

View File

@ -16,7 +16,7 @@ const TestResults = ({ results, assertionResults }) => {
return (
<StyledWrapper className="flex flex-col px-3">
<div className="py-2 font-medium test-summary">
<div className="pb-2 font-medium test-summary">
Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length}
</div>
<ul className="">

View File

@ -61,15 +61,15 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
}
};
if (isLoading) {
if (isLoading && !item.response) {
return (
<StyledWrapper className="flex h-full relative">
<StyledWrapper className="flex flex-col h-full relative">
<Overlay item={item} collection={collection} />
</StyledWrapper>
);
}
if (response.state !== 'success') {
if (!item.response) {
return (
<StyledWrapper className="flex h-full relative">
<Placeholder />
@ -115,7 +115,10 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
</div>
) : null}
</div>
<section className="flex flex-grow">{getTabPanel(focusedTab.responsePaneTab)}</section>
<section className={`flex flex-grow ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}>
{isLoading ? <Overlay item={item} collection={collection} /> : null}
{getTabPanel(focusedTab.responsePaneTab)}
</section>
</StyledWrapper>
);
};

View File

@ -18,7 +18,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),

View File

@ -17,7 +17,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),

View File

@ -16,7 +16,7 @@ const RenameCollection = ({ collection, onClose }) => {
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),

View File

@ -21,11 +21,11 @@ const CreateCollection = ({ onClose }) => {
},
validationSchema: Yup.object({
collectionName: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('collection name is required'),
collectionFolderName: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
.required('folder name is required'),

View File

@ -16,7 +16,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
},
validationSchema: Yup.object({
collectionLocation: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(500, 'must be 500 characters or less')
.required('name is required')
}),

View File

@ -16,7 +16,7 @@ const NewFolder = ({ collection, item, onClose }) => {
},
validationSchema: Yup.object({
folderName: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.required('name is required')
.test({
name: 'folderName',

View File

@ -25,7 +25,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
},
validationSchema: Yup.object({
requestName: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.required('name is required')
.test({
name: 'requestName',

View File

@ -18,6 +18,7 @@ const TitleBar = () => {
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
const dispatch = useDispatch();
const { ipcRenderer } = window;
const handleImportCollection = (collection) => {
setImportedCollection(collection);
@ -50,6 +51,10 @@ const TitleBar = () => {
);
};
const openDevTools = () => {
ipcRenderer.invoke('renderer:open-devtools');
};
return (
<StyledWrapper className="px-2 py-2">
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} /> : null}
@ -104,6 +109,15 @@ const TitleBar = () => {
>
Import Collection
</div>
<div
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
openDevTools();
}}
>
Devtools
</div>
</Dropdown>
</div>
</div>

View File

@ -105,7 +105,7 @@ const Sidebar = () => {
Star
</GitHubButton>
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.21.0</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.21.1</div>
</div>
</div>
</div>

View File

@ -129,6 +129,24 @@ const GlobalStyle = createGlobalStyle`
}
}
@keyframes rotateClockwise {
0% {
transform: scaleY(-1) rotate(0deg);
}
100% {
transform: scaleY(-1) rotate(360deg);
}
}
@keyframes rotateCounterClockwise {
0% {
transform: scaleY(-1) rotate(360deg);
}
100% {
transform: scaleY(-1) rotate(0deg);
}
}
// codemirror
.CodeMirror {
.cm-variable-valid {

View File

@ -82,8 +82,12 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
itemSchema
.validate(itemToSave)
.then(() => ipcRenderer.invoke('renderer:save-request', item.pathname, itemToSave))
.then(() => toast.success('Request saved successfully'))
.then(resolve)
.catch(reject);
.catch((err) => {
toast.error('Failed to save request!');
reject(err);
});
});
};

View File

@ -258,6 +258,17 @@ export const collectionsSlice = createSlice({
}
}
},
deleteRequestDraft: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && item.draft) {
item.draft = null;
}
}
},
newEphemeralHttpRequest: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -1103,7 +1114,6 @@ export const collectionsSlice = createSlice({
const { cancelTokenUid } = action.payload;
item.requestUid = requestUid;
item.requestState = 'queued';
item.response = null;
item.cancelTokenUid = cancelTokenUid;
}
@ -1224,6 +1234,7 @@ export const {
requestCancelled,
responseReceived,
saveRequest,
deleteRequestDraft,
newEphemeralHttpRequest,
collectionClicked,
collectionFolderClicked,

View File

@ -53,3 +53,9 @@ body::-webkit-scrollbar-thumb,
background-color: #cdcdcd;
border-radius: 5rem;
}
/* making all the checkboxes and radios bigger */
input[type='checkbox'],
input[type='radio'] {
transform: scale(1.25);
}

View File

@ -109,7 +109,8 @@ const darkTheme = {
responseSendIcon: '#555',
responseStatus: '#ccc',
responseOk: '#8cd656',
responseError: '#f06f57'
responseError: '#f06f57',
responseOverlayBg: 'rgba(30, 30, 30, 0.6)'
},
collection: {

View File

@ -109,7 +109,8 @@ const lightTheme = {
responseSendIcon: 'rgb(209, 213, 219)',
responseStatus: 'rgb(117 117 117)',
responseOk: '#047857',
responseError: 'rgb(185, 28, 28)'
responseError: 'rgb(185, 28, 28)',
responseOverlayBg: 'rgba(255, 255, 255, 0.6)'
},
collection: {

View File

@ -41,3 +41,10 @@ export const isWindowsOS = () => {
return osFamily.includes('windows');
};
export const isMacOS = () => {
const os = platform.os;
const osFamily = os.family.toLowerCase();
return osFamily.includes('os x');
};

View File

@ -29,14 +29,11 @@ const sendHttpRequest = async (item, collection, environment, collectionVariable
});
};
export const fetchGqlSchema = async (endpoint, environment, request, collectionVariables) => {
export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer
.invoke('fetch-gql-schema', endpoint, environment, request, collectionVariables)
.then(resolve)
.catch(reject);
ipcRenderer.invoke('fetch-gql-schema', endpoint, environment, request, collection).then(resolve).catch(reject);
});
};

View File

@ -1,5 +1,5 @@
{
"version": "v0.21.0",
"version": "v0.21.1",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",

View File

@ -24,7 +24,10 @@ const template = [
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'selectAll' }
{ role: 'selectAll' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' }
]
},
{

View File

@ -2,8 +2,8 @@ const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const chokidar = require('chokidar');
const { hasBruExtension, writeFile } = require('../utils/filesystem');
const { bruToEnvJson, envJsonToBru, bruToJson, jsonToBru } = require('../bru');
const { hasBruExtension } = require('../utils/filesystem');
const { bruToEnvJson, bruToJson } = require('../bru');
const { dotenvToJson } = require('@usebruno/lang');
const { uuid } = require('../utils/common');
@ -15,13 +15,6 @@ const EnvironmentSecretsStore = require('../store/env-secrets');
const environmentSecretsStore = new EnvironmentSecretsStore();
const isJsonEnvironmentConfig = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const basename = path.basename(pathname);
return dirname === collectionPath && basename === 'environments.json';
};
const isDotEnvFile = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const basename = path.basename(pathname);
@ -198,32 +191,6 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
}
}
if (isJsonEnvironmentConfig(pathname, collectionPath)) {
try {
const dirname = path.dirname(pathname);
const bruContent = fs.readFileSync(pathname, 'utf8');
const jsonData = JSON.parse(bruContent);
const envDirectory = path.join(dirname, 'environments');
if (!fs.existsSync(envDirectory)) {
fs.mkdirSync(envDirectory);
}
for (const env of jsonData) {
const bruEnvFilename = path.join(envDirectory, `${env.name}.bru`);
const bruContent = envJsonToBru(env);
await writeFile(bruEnvFilename, bruContent);
}
await fs.unlinkSync(pathname);
} catch (err) {
// do nothing
}
return;
}
if (isBruEnvironmentConfig(pathname, collectionPath)) {
return addEnvironmentFile(win, pathname, collectionUid, collectionPath);
}

View File

@ -1,6 +1,5 @@
const _ = require('lodash');
const { bruToJsonV2, jsonToBruV2, bruToEnvJsonV2, envJsonToBruV2 } = require('@usebruno/lang');
const { each } = require('lodash');
const bruToEnvJson = (bru) => {
try {
@ -10,7 +9,7 @@ const bruToEnvJson = (bru) => {
// this need to be evaluated and safely removed
// i don't see it being used in schema validation
if (json && json.variables && json.variables.length) {
each(json.variables, (v) => (v.type = 'text'));
_.each(json.variables, (v) => (v.type = 'text'));
}
return json;

View File

@ -9,6 +9,7 @@ const LastOpenedCollections = require('./store/last-opened-collections');
const registerNetworkIpc = require('./ipc/network');
const registerCollectionsIpc = require('./ipc/collection');
const Watcher = require('./app/watcher');
const { loadWindowState, saveWindowState } = require('./utils/window');
const lastOpenedCollections = new LastOpenedCollections();
@ -27,9 +28,13 @@ let watcher;
// Prepare the renderer once the app is ready
app.on('ready', async () => {
const { x, y, width, height } = loadWindowState();
mainWindow = new BrowserWindow({
width: 1280,
height: 768,
x,
y,
width,
height,
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
@ -54,6 +59,9 @@ app.on('ready', async () => {
mainWindow.loadURL(url);
watcher = new Watcher();
mainWindow.on('resize', () => saveWindowState(mainWindow));
mainWindow.on('move', () => saveWindowState(mainWindow));
mainWindow.webContents.on('new-window', function (e, url) {
e.preventDefault();
require('electron').shell.openExternal(url);

View File

@ -486,6 +486,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
return Promise.reject(error);
}
});
ipcMain.handle('renderer:open-devtools', async () => {
mainWindow.webContents.openDevTools();
});
};
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {

View File

@ -469,7 +469,7 @@ const registerNetworkIpc = (mainWindow) => {
});
});
ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, request, collectionVariables) => {
ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, request, collection) => {
try {
const envVars = getEnvVars(environment);
const preparedRequest = prepareGqlIntrospectionRequest(endpoint, envVars, request);
@ -483,7 +483,8 @@ const registerNetworkIpc = (mainWindow) => {
});
}
interpolateVars(preparedRequest, envVars, collectionVariables);
const processEnvVars = getProcessEnvVars(collection.uid);
interpolateVars(preparedRequest, envVars, collection.collectionVariables, processEnvVars);
const response = await axios(preparedRequest);

View File

@ -1,16 +1,12 @@
const Mustache = require('mustache');
const Handlebars = require('handlebars');
const { getIntrospectionQuery } = require('graphql');
const { get } = require('lodash');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
return value;
};
const prepareGqlIntrospectionRequest = (endpoint, envVars, request) => {
if (endpoint && endpoint.length) {
endpoint = Mustache.render(endpoint, envVars);
endpoint = Handlebars.compile(endpoint, { noEscape: true })(envVars);
}
const introspectionQuery = getIntrospectionQuery();
const queryParams = {
query: introspectionQuery
@ -20,6 +16,7 @@ const prepareGqlIntrospectionRequest = (endpoint, envVars, request) => {
method: 'POST',
url: endpoint,
headers: {
...mapHeaders(request.headers),
Accept: 'application/json',
'Content-Type': 'application/json'
},
@ -42,4 +39,10 @@ const prepareGqlIntrospectionRequest = (endpoint, envVars, request) => {
return axiosRequest;
};
const mapHeaders = (headers) => {
const entries = headers.filter((header) => header.enabled).map(({ name, value }) => [name, value]);
return Object.fromEntries(entries);
};
module.exports = prepareGqlIntrospectionRequest;

View File

@ -0,0 +1,31 @@
const _ = require('lodash');
const Store = require('electron-store');
const DEFAULT_WINDOW_WIDTH = 1280;
const DEFAULT_WINDOW_HEIGHT = 768;
class WindowStateStore {
constructor() {
this.store = new Store({
name: 'preferences',
clearInvalidConfig: true
});
}
getBounds() {
return (
this.store.get('window-bounds') || {
x: 0,
y: 0,
width: DEFAULT_WINDOW_WIDTH,
height: DEFAULT_WINDOW_HEIGHT
}
);
}
setBounds(bounds) {
this.store.set('window-bounds', bounds);
}
}
module.exports = WindowStateStore;

View File

@ -0,0 +1,53 @@
const { screen } = require('electron');
const WindowStateStore = require('../store/window-state');
const windowStateStore = new WindowStateStore();
const DEFAULT_WINDOW_WIDTH = 1280;
const DEFAULT_WINDOW_HEIGHT = 768;
const loadWindowState = () => {
const bounds = windowStateStore.getBounds();
const positionValid = isPositionValid(bounds);
const sizeValid = isSizeValid(bounds);
return {
x: bounds.x && positionValid ? bounds.x : undefined,
y: bounds.y && positionValid ? bounds.y : undefined,
width: bounds.width && sizeValid ? bounds.width : DEFAULT_WINDOW_WIDTH,
height: bounds.height && sizeValid ? bounds.height : DEFAULT_WINDOW_HEIGHT
};
};
const saveWindowState = (window) => {
const bounds = window.getBounds();
windowStateStore.setBounds(bounds);
};
const isPositionValid = (bounds) => {
const area = getArea(bounds);
return (
bounds.x >= area.x &&
bounds.y >= area.y &&
bounds.x + bounds.width <= area.x + area.width &&
bounds.y + bounds.height <= area.y + area.height
);
};
const isSizeValid = (bounds) => {
const area = getArea(bounds);
return bounds.width <= area.width && bounds.height <= area.height;
};
const getArea = (bounds) => {
return screen.getDisplayMatching(bounds).workArea;
};
module.exports = {
loadWindowState,
saveWindowState
};

View File

@ -268,7 +268,7 @@ class AssertRuntime {
expect(lhs).to.endWith(rhs);
break;
case 'between':
const [min, max] = value.split(',');
const [min, max] = rhs;
expect(lhs).to.be.within(min, max);
break;
case 'isEmpty':

View File

@ -0,0 +1,273 @@
const ohm = require('ohm-js');
const _ = require('lodash');
const { outdentString } = require('../../v1/src/utils');
const grammar = ohm.grammar(`Bru {
BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
auths = authbasic | authbearer
nl = "\\r"? "\\n"
st = " " | "\\t"
stnl = st | nl
tagend = nl "}"
optionalnl = ~tagend nl
keychar = ~(tagend | st | nl | ":") any
valuechar = ~(nl | tagend) any
// Dictionary Blocks
dictionary = st* "{" pairlist? tagend
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
pair = st* key st* ":" st* value st*
key = keychar*
value = valuechar*
// Text Blocks
textblock = textline (~tagend nl textline)*
textline = textchar*
textchar = ~nl any
meta = "meta" dictionary
auth = "auth" dictionary
headers = "headers" dictionary
query = "query" dictionary
vars = varsreq | varsres
varsreq = "vars:pre-request" dictionary
varsres = "vars:post-response" dictionary
authbasic = "auth:basic" dictionary
authbearer = "auth:bearer" dictionary
script = scriptreq | scriptres
scriptreq = "script:pre-request" st* "{" nl* textblock tagend
scriptres = "script:post-response" st* "{" nl* textblock tagend
tests = "tests" st* "{" nl* textblock tagend
docs = "docs" st* "{" nl* textblock tagend
}`);
const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
if (!pairList.length) {
return [];
}
return _.map(pairList[0], (pair) => {
let name = _.keys(pair)[0];
let value = pair[name];
if (!parseEnabled) {
return {
name,
value
};
}
let enabled = true;
if (name && name.length && name.charAt(0) === '~') {
name = name.slice(1);
enabled = false;
}
return {
name,
value,
enabled
};
});
};
const concatArrays = (objValue, srcValue) => {
if (_.isArray(objValue) && _.isArray(srcValue)) {
return objValue.concat(srcValue);
}
};
const mapPairListToKeyValPair = (pairList = []) => {
if (!pairList || !pairList.length) {
return {};
}
return _.merge({}, ...pairList[0]);
};
const sem = grammar.createSemantics().addAttribute('ast', {
BruFile(tags) {
if (!tags || !tags.ast || !tags.ast.length) {
return {};
}
return _.reduce(
tags.ast,
(result, item) => {
return _.mergeWith(result, item, concatArrays);
},
{}
);
},
dictionary(_1, _2, pairlist, _3) {
return pairlist.ast;
},
pairlist(_1, pair, _2, rest, _3) {
return [pair.ast, ...rest.ast];
},
pair(_1, key, _2, _3, _4, value, _5) {
let res = {};
res[key.ast] = value.ast ? value.ast.trim() : '';
return res;
},
key(chars) {
return chars.sourceString ? chars.sourceString.trim() : '';
},
value(chars) {
return chars.sourceString ? chars.sourceString.trim() : '';
},
textblock(line, _1, rest) {
return [line.ast, ...rest.ast].join('\n');
},
textline(chars) {
return chars.sourceString;
},
textchar(char) {
return char.sourceString;
},
nl(_1, _2) {
return '';
},
st(_) {
return '';
},
tagend(_1, _2) {
return '';
},
_iter(...elements) {
return elements.map((e) => e.ast);
},
meta(_1, dictionary) {
let meta = mapPairListToKeyValPair(dictionary.ast) || {};
meta.type = 'collection';
return {
meta
};
},
auth(_1, dictionary) {
let auth = mapPairListToKeyValPair(dictionary.ast) || {};
return {
auth: {
mode: auth ? auth.mode : 'none'
}
};
},
query(_1, dictionary) {
return {
query: mapPairListToKeyValPairs(dictionary.ast)
};
},
headers(_1, dictionary) {
return {
headers: mapPairListToKeyValPairs(dictionary.ast)
};
},
authbasic(_1, dictionary) {
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
const usernameKey = _.find(auth, { name: 'username' });
const passwordKey = _.find(auth, { name: 'password' });
const username = usernameKey ? usernameKey.value : '';
const password = passwordKey ? passwordKey.value : '';
return {
auth: {
basic: {
username,
password
}
}
};
},
authbearer(_1, dictionary) {
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
const tokenKey = _.find(auth, { name: 'token' });
const token = tokenKey ? tokenKey.value : '';
return {
auth: {
bearer: {
token
}
}
};
},
varsreq(_1, dictionary) {
const vars = mapPairListToKeyValPairs(dictionary.ast);
_.each(vars, (v) => {
let name = v.name;
if (name && name.length && name.charAt(0) === '@') {
v.name = name.slice(1);
v.local = true;
} else {
v.local = false;
}
});
return {
vars: {
req: vars
}
};
},
varsres(_1, dictionary) {
const vars = mapPairListToKeyValPairs(dictionary.ast);
_.each(vars, (v) => {
let name = v.name;
if (name && name.length && name.charAt(0) === '@') {
v.name = name.slice(1);
v.local = true;
} else {
v.local = false;
}
});
return {
vars: {
res: vars
}
};
},
scriptreq(_1, _2, _3, _4, textblock, _5) {
return {
script: {
req: outdentString(textblock.sourceString)
}
};
},
scriptres(_1, _2, _3, _4, textblock, _5) {
return {
script: {
res: outdentString(textblock.sourceString)
}
};
},
tests(_1, _2, _3, _4, textblock, _5) {
return {
tests: outdentString(textblock.sourceString)
};
},
docs(_1, _2, _3, _4, textblock, _5) {
return {
docs: outdentString(textblock.sourceString)
};
}
});
const parser = (input) => {
const match = grammar.match(input);
if (match.succeeded()) {
return sem(match).ast;
} else {
throw new Error(match.message);
}
};
module.exports = parser;

View File

@ -0,0 +1,185 @@
const _ = require('lodash');
const { indentString } = require('../../v1/src/utils');
const enabled = (items = []) => items.filter((item) => item.enabled);
const disabled = (items = []) => items.filter((item) => !item.enabled);
// remove the last line if two new lines are found
const stripLastLine = (text) => {
if (!text || !text.length) return text;
return text.replace(/(\r?\n)$/, '');
};
const jsonToBru = (json) => {
const { meta, query, headers, auth, script, tests, vars, docs } = json;
let bru = '';
if (meta) {
bru += 'meta {\n';
for (const key in meta) {
bru += ` ${key}: ${meta[key]}\n`;
}
bru += '}\n\n';
}
if (query && query.length) {
bru += 'query {';
if (enabled(query).length) {
bru += `\n${indentString(
enabled(query)
.map((item) => `${item.name}: ${item.value}`)
.join('\n')
)}`;
}
if (disabled(query).length) {
bru += `\n${indentString(
disabled(query)
.map((item) => `~${item.name}: ${item.value}`)
.join('\n')
)}`;
}
bru += '\n}\n\n';
}
if (headers && headers.length) {
bru += 'headers {';
if (enabled(headers).length) {
bru += `\n${indentString(
enabled(headers)
.map((item) => `${item.name}: ${item.value}`)
.join('\n')
)}`;
}
if (disabled(headers).length) {
bru += `\n${indentString(
disabled(headers)
.map((item) => `~${item.name}: ${item.value}`)
.join('\n')
)}`;
}
bru += '\n}\n\n';
}
if (auth && auth.mode) {
bru += `auth {
${indentString(`mode: ${auth.mode}`)}
}
`;
}
if (auth && auth.basic) {
bru += `auth:basic {
${indentString(`username: ${auth.basic.username}`)}
${indentString(`password: ${auth.basic.password}`)}
}
`;
}
if (auth && auth.bearer) {
bru += `auth:bearer {
${indentString(`token: ${auth.bearer.token}`)}
}
`;
}
let reqvars = _.get(vars, 'req');
let resvars = _.get(vars, 'res');
if (reqvars && reqvars.length) {
const varsEnabled = _.filter(reqvars, (v) => v.enabled && !v.local);
const varsDisabled = _.filter(reqvars, (v) => !v.enabled && !v.local);
const varsLocalEnabled = _.filter(reqvars, (v) => v.enabled && v.local);
const varsLocalDisabled = _.filter(reqvars, (v) => !v.enabled && v.local);
bru += `vars:pre-request {`;
if (varsEnabled.length) {
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
}
if (varsLocalEnabled.length) {
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`;
}
if (varsDisabled.length) {
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`;
}
if (varsLocalDisabled.length) {
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`;
}
bru += '\n}\n\n';
}
if (resvars && resvars.length) {
const varsEnabled = _.filter(resvars, (v) => v.enabled && !v.local);
const varsDisabled = _.filter(resvars, (v) => !v.enabled && !v.local);
const varsLocalEnabled = _.filter(resvars, (v) => v.enabled && v.local);
const varsLocalDisabled = _.filter(resvars, (v) => !v.enabled && v.local);
bru += `vars:post-response {`;
if (varsEnabled.length) {
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
}
if (varsLocalEnabled.length) {
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`;
}
if (varsDisabled.length) {
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`;
}
if (varsLocalDisabled.length) {
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`;
}
bru += '\n}\n\n';
}
if (script && script.req && script.req.length) {
bru += `script:pre-request {
${indentString(script.req)}
}
`;
}
if (script && script.res && script.res.length) {
bru += `script:post-response {
${indentString(script.res)}
}
`;
}
if (tests && tests.length) {
bru += `tests {
${indentString(tests)}
}
`;
}
if (docs && docs.length) {
bru += `docs {
${indentString(docs)}
}
`;
}
return stripLastLine(bru);
};
module.exports = jsonToBru;

View File

@ -0,0 +1,24 @@
const fs = require('fs');
const path = require('path');
const collectionBruToJson = require('../src/collectionBruToJson');
const jsonToCollectionBru = require('../src/jsonToCollectionBru');
describe('collectionBruToJson', () => {
it('should parse the collection bru file', () => {
const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'collection.bru'), 'utf8');
const expected = require('./fixtures/collection.json');
const output = collectionBruToJson(input);
expect(output).toEqual(expected);
});
});
describe('jsonToCollectionBru', () => {
it('should convert the collection json to bru', () => {
const input = require('./fixtures/collection.json');
const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'collection.bru'), 'utf8');
const output = jsonToCollectionBru(input);
expect(output).toEqual(expected);
});
});

View File

@ -0,0 +1,43 @@
meta {
type: collection
}
headers {
content-type: application/json
Authorization: Bearer 123
~transaction-id: {{transactionId}}
}
auth {
mode: none
}
auth:basic {
username: john
password: secret
}
auth:bearer {
token: 123
}
vars:pre-request {
departingDate: 2020-01-01
~returningDate: 2020-01-02
}
vars:post-response {
~transactionId: $res.body.transactionId
}
script:pre-request {
console.log("In Collection pre Request Script");
}
script:post-response {
console.log("In Collection post Request Script");
}
docs {
This request needs auth token to be set in the headers.
}

View File

@ -0,0 +1,61 @@
{
"meta": {
"type": "collection"
},
"headers": [
{
"name": "content-type",
"value": "application/json",
"enabled": true
},
{
"name": "Authorization",
"value": "Bearer 123",
"enabled": true
},
{
"name": "transaction-id",
"value": "{{transactionId}}",
"enabled": false
}
],
"auth": {
"mode": "none",
"basic": {
"username": "john",
"password": "secret"
},
"bearer": {
"token": "123"
}
},
"vars": {
"req": [
{
"name": "departingDate",
"value": "2020-01-01",
"enabled": true,
"local": false
},
{
"name": "returningDate",
"value": "2020-01-02",
"enabled": false,
"local": false
}
],
"res": [
{
"name": "transactionId",
"value": "$res.body.transactionId",
"enabled": false,
"local": false
}
]
},
"script": {
"req": "console.log(\"In Collection pre Request Script\");",
"res": "console.log(\"In Collection post Request Script\");"
},
"docs": "This request needs auth token to be set in the headers."
}

View File

@ -123,7 +123,7 @@ const itemSchema = Yup.object({
uid: uidSchema,
type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder']).required('type is required'),
seq: Yup.number().min(1),
name: Yup.string().min(1, 'name must be atleast 1 characters').required('name is required'),
name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'),
request: requestSchema.when('type', {
is: (type) => ['http-request', 'graphql-request'].includes(type),
then: (schema) => schema.required('request is required when item-type is request')
@ -138,7 +138,7 @@ const itemSchema = Yup.object({
const collectionSchema = Yup.object({
version: Yup.string().oneOf(['1']).required('version is required'),
uid: uidSchema,
name: Yup.string().min(1, 'name must be atleast 1 characters').required('name is required'),
name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'),
items: Yup.array().of(itemSchema),
activeEnvironmentUid: Yup.string()
.length(21, 'activeEnvironmentUid must be 21 characters in length')

View File

@ -34,7 +34,7 @@ describe('Item Schema Validation', () => {
return Promise.all([
expect(itemSchema.validate(item)).rejects.toEqual(
validationErrorWithMessages('name must be atleast 1 characters')
validationErrorWithMessages('name must be at least 1 character')
)
]);
});